createIssueStickerHeadless(config)가 돌려주는 클라이언트가 가진 메서드 전체입니다.
생명주기와 상태 구독
initialize()
initialize(): Promise<void>
클라이언트를 준비 상태(idle)로 만듭니다. API key를 검증하고 프로젝트 정보를 불러옵니다. 다른 메서드들도 필요하면 내부에서 먼저 초기화하므로, 보통 앱을 켤 때 한 번만 명시적으로 호출하면 됩니다.
subscribe(listener)
subscribe(listener: (snapshot: HeadlessSnapshot) => void): () => void
상태가 바뀔 때마다 listener를 호출합니다. 반환된 함수를 호출하면 구독이 해제됩니다. UI는 이 콜백 안에서 다시 그리면 됩니다.
getSnapshot()
getSnapshot(): HeadlessSnapshot
현재 상태를 한 번 동기적으로 읽습니다. 구독 없이 즉시 값이 필요할 때 사용합니다.
phase
readonly phase: HeadlessPhase
현재 phase를 읽는 읽기 전용 속성입니다. getSnapshot().phase의 단축입니다.
destroy()
destroy(): void
클라이언트를 정리합니다. 구독·리스너를 해제하고 진행 중인 녹화·세션 리플레이를 중단하며 내부 상태를 초기화합니다. 컴포넌트를 언마운트하거나 위젯을 내릴 때 호출하세요.
이슈 불러오기
refreshIssues(filters?)
refreshIssues(filters?: HeadlessIssueFilters): Promise<void>
조건에 맞는 이슈 목록을 불러와 snapshot.issues에 채웁니다. idle 또는 viewingIssue에서 호출할 수 있습니다.
// 현재 페이지의 내 이슈만
await client.refreshIssues({
url: window.location.href,
externalUserId: currentUser.id,
});
// 전체 페이지에서 검색·필터
await client.refreshIssues({
url: 'all',
search: 'checkout',
customFieldValues: { [fieldDefinitionId]: ['high'] },
});
식별된 사용자(user 또는 externalUserId)가 없으면 public 모드에서는 목록이 비어 있습니다. 멤버로 로그인하면 프로젝트 전체 이슈를 불러올 수 있습니다.
openIssue(issueId)
openIssue(issueId: string): Promise<void>
이슈 상세를 불러와 snapshot.activeIssue에 담고 phase를 viewingIssue로 바꿉니다.
closeIssue()
closeIssue(): void
상세 보기를 닫고 idle로 돌아갑니다.
refreshFilterSchema()
refreshFilterSchema(): Promise<void>
필터 UI를 그릴 정의(snapshot.filterSchema.groups)를 불러옵니다. 필터는 멤버 모드에서만 제공됩니다.
이슈 만들기
startIssueCapture(options?)
startIssueCapture(options?: HeadlessIssueCaptureOptions): Promise<void>
스크린샷 이슈 만들기를 시작합니다. phase가 selectingTarget이 되고, 사용자가 페이지 요소를 클릭하면 즉시 초안(snapshot.draft)이 만들어지고 draftingIssue로 전환됩니다. 스크린샷은 백그라운드에서 캡처되어 완료되는 대로 snapshot.draft.screenshotPreviewUrls에 채워지므로, 폼을 바로 그리되 프리뷰 영역은 빈 배열일 때 로딩으로 표시하면 됩니다.
제목·설명·동적 필드 값은 앱이 직접 상태로 관리하고, 제출할 때 submitDraft에 넘깁니다.
await client.startIssueCapture();
const form = { title: '', description: '', fields: {} as Record<string, unknown> };
client.subscribe((snapshot) => {
if (snapshot.phase !== 'draftingIssue' || !snapshot.draft) return;
renderIssueForm({
screenshots: snapshot.draft.screenshotPreviewUrls, // []이면 캡처 진행 중
fields: snapshot.formSchema?.fields ?? [], // null이면 스키마 로딩 중
onTitleChange: (title) => (form.title = title),
onFieldChange: (id, value) => (form.fields[id] = value),
onSubmit: () =>
client.submitDraft({
title: form.title,
description: form.description,
dynamicFields: form.fields,
}),
onCancel: () => client.cancelIssueCapture(),
});
});
startVideoCapture()
startVideoCapture(): Promise<void>
화면 녹화를 시작합니다(phase recordingVideo). 브라우저의 화면 공유 권한을 사용합니다.
stopVideoCapture()
stopVideoCapture(): Promise<void>
녹화를 끝냅니다. 녹화 파일이 있으면 요소 선택 단계(selectingTarget)로 이어지고, 사용자가 요소를 클릭하면 녹화 영상이 초안에 연결됩니다. 사용자가 브라우저 공유 UI에서 직접 녹화를 멈춰도 동일하게 처리됩니다.
setSessionReplayEnabled(enabled)
setSessionReplayEnabled(enabled: boolean): void
이슈를 제출할 때 세션 리플레이(직전 조작 다시보기)를 함께 첨부할지 토글합니다. snapshot.sessionReplay의 available·canAttach로 사용 가능 여부를 확인해 스위치를 그리세요.
client.subscribe((snapshot) => {
renderReplayToggle({
available: snapshot.sessionReplay.available,
checked: snapshot.sessionReplay.enabled,
onChange: (on) => client.setSessionReplayEnabled(on),
});
});
cancelIssueCapture()
cancelIssueCapture(): void
진행 중인 캡처·녹화·초안 작성을 취소하고 idle로 되돌립니다.
submitDraft(input)
submitDraft(input: {
title: string;
description: string;
dynamicFields?: Record<string, unknown>;
}): Promise<Issue>
앱이 관리하던 제목·설명·동적 필드 값을 받아 이슈를 생성하고, 생성된 Issue를 반환합니다. 동적 필드의 키는 snapshot.formSchema.fields의 id입니다(DynamicFieldDescriptor 참고). 스크린샷·영상·콘솔/네트워크 로그·세션 리플레이는 자동으로 함께 업로드됩니다.
- 제목·설명이 비어 있으면
VALIDATION_ERROR로 거부됩니다. - 호출 시점에 스크린샷 캡처가 아직 진행 중이면 phase가
submittingIssue로 전환된 채 캡처 완료를 기다렸다가 업로드합니다.submittingIssue동안 제출 버튼 비활성화·스피너 같은 진행 피드백을 phase 기준으로 그리세요.
유저 액션 없이 이슈 만들기
createIssue(input)
createIssue(input: {
title: string;
description: string;
dynamicFields?: Record<string, unknown>;
target?: HTMLElement;
media?: 'screenshot' | 'none';
}): Promise<Issue | null>
사용자가 화면에서 요소를 고르는 캡처 단계를 거치지 않고, 앱 코드가 조건을 판단해 이슈를 생성합니다. 스크린샷·콘솔/네트워크 로그·세션 리플레이는 submitDraft와 동일하게 자동으로 업로드됩니다.
사용자가 버그를 만나도 신고까지 오는 일은 많지 않습니다. 화면이 멈추면 대부분 새로고침하고 넘어가고, 저장이 안 되면 다시 시도해 보다 그만둡니다. 그래서 정작 기록이 필요한 상황일수록 이슈가 남지 않습니다. 이 메서드는 그 순간을 코드가 직접 잡아 기록합니다.
화면이 죽어 사용자가 아무것도 할 수 없는 순간을 기록합니다. 렌더가 예외로 중단되면 사용자에게는 빈 화면만 보입니다. 무엇을 하다 그렇게 됐는지는 사용자도 설명하지 못하지만, 세션 리플레이와 콘솔 로그에는 직전 조작이 그대로 기록됩니다.
class AppErrorBoundary extends React.Component<Props, State> {
componentDidCatch(error: Error, info: React.ErrorInfo) {
void client.createIssue({
title: `렌더링 중단 · ${error.name}`,
description: [error.message, info.componentStack].join('\n\n'),
target: this.containerRef.current ?? undefined,
});
}
}
target으로 죽은 영역을 넘기면 그 요소에 마커가 붙어, 이슈 목록에서 화면의 어느 부분이 무너졌는지 바로 보입니다.
사용자 환경에서만 재현되는 실패를 선제적으로 감지합니다. 테스트 환경에서는 늘 통과하지만 특정 사용자의 파일 크기·네트워크·권한 조합에서만 실패하는 경로가 있습니다. 사용자는 재시도하다 포기할 뿐 신고하지 않으므로, 실패했다는 사실 자체가 팀에 도달하지 않습니다.
const result = await uploadAttachment(file);
if (!result.ok) {
void client.createIssue({
title: '첨부 업로드 실패',
description: `${file.name} · ${Math.round(file.size / 1024)}KB · ${result.status}`,
media: 'none',
});
}
옵션과 동작은 다음과 같습니다.
- 실패해도 예외를 던지지 않고
null을 돌려줍니다. 이미 오류를 처리하는 자리에서 부르는 메서드라, 이슈 생성만을 위한try/catch를 한 겹 더 쓰지 않아도 됩니다. 실패 사유는 콘솔 경고로 남습니다. target을 넘기지 않으면document.body에 붙습니다. 이슈 마커는 문서 좌상단에 표시됩니다.media: 'none'이면 스크린샷 캡처를 건너뛰고 즉시 전송합니다. 화면 상태보다 로그가 중요하거나, 캡처에 걸리는 시간이 부담될 때 사용합니다.phase와draft를 바꾸지 않습니다. 사용자가 이슈를 작성하는 중에 호출해도 그 작업에 영향을 주지 않습니다.- 이슈스티커 익스텐션이 같은 페이지를 사용 중이면 이슈를 생성하지 않습니다.
- 호출 조건이 잘못 잡히면 이슈가 무한히 쌓이므로 SDK가 60초에 5건으로 제한합니다. 넘어선 호출은 서버로 나가지 않습니다.
멤버 로그인
loginAsMember()
loginAsMember(): Promise<void>
워크스페이스 멤버로 로그인합니다. 가능한 경우 조용히 로그인하고, 아니면 로그인 창을 띄웁니다. 멤버로 로그인하면 프로젝트 전체 이슈 조회와 동적 필드·필터를 사용할 수 있습니다. 멤버 자격 증명은 SDK 메모리에만 보관되며 앱이 직접 저장하지 않습니다.
logoutMember()
logoutMember(): Promise<void>
멤버 로그아웃하고 public 모드로 돌아갑니다.