issue-sticker 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,34 @@
1
- import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadToSignedUrl, n as normalizeExposedJiraFields, b as normalizeJiraFieldId, g as getElementPosition, c as captureWithModernScreenshot, e as buildJiraCreateFields, f as buildNotionCreateFields, h as buildGuestOwnerQuery, s as syncExternalIssueIntegrations, i as devLogger, C as ConsoleCollector, N as NetworkCollector, j as filterExcludeServiceRelatedLogs, k as filterExcludeServiceRelatedConsoleLogs, l as compactNetworkLogsForTransport, m as sanitizeString, o as buildExternalUserPayload, S as ScreenRecordingController, p as SessionReplayCaptureController, q as preloadModernScreenshot } from "./externalUserPayload-hJjuUx6q.js";
1
+ import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadToSignedUrl, p as pristineFetch, n as normalizeExposedJiraFields, b as normalizeJiraFieldId, g as getElementPosition, c as captureWithModernScreenshot, e as buildJiraCreateFields, f as buildNotionCreateFields, h as buildGuestOwnerQuery, i as buildIssueIntegrationsPayload, j as devLogger, C as ConsoleCollector, N as NetworkCollector, s as selectNetworkLogsForSubmit, k as filterExcludeServiceRelatedLogs, l as filterExcludeServiceRelatedConsoleLogs, m as compactNetworkLogsForTransport, o as CONSOLE_LOG_SUBMIT_LIMIT, q as buildExternalUserPayload, r as shrinkNetworkLogsToFit, t as measureJsonBytes, v as sanitizeString, w as getCurrentPageUrl, S as ScreenRecordingController, x as SessionReplayCaptureController, y as preloadModernScreenshot, I as ISSUE_STICKER_INTERNAL_LOG_PREFIX, z as isIssueStickerExtensionRuntimeActive } from "./externalUserPayload-zu5IXBjn.js";
2
2
  import "react/jsx-runtime";
3
3
  import "react";
4
- import "./createLucideIcon-CmSNiW63.js";
4
+ import "./createLucideIcon-BzRMP02h.js";
5
+ const WINDOW_MS = 6e4;
6
+ const MAX_PER_WINDOW = 5;
7
+ class AutoIssueRateLimit {
8
+ constructor(max = MAX_PER_WINDOW, windowMs = WINDOW_MS) {
9
+ this.max = max;
10
+ this.windowMs = windowMs;
11
+ this.accepted = [];
12
+ }
13
+ get label() {
14
+ return `${this.windowMs / 1e3}초에 ${this.max}건`;
15
+ }
16
+ /**
17
+ * 자리가 있으면 차지하고 true 를 돌려준다. 실패한 호출도 서버 왕복을 이미 썼으므로
18
+ * 자리를 돌려주지 않는다.
19
+ */
20
+ claim(now = Date.now()) {
21
+ this.accepted = this.accepted.filter((at) => now - at < this.windowMs);
22
+ if (this.accepted.length >= this.max) {
23
+ return false;
24
+ }
25
+ this.accepted.push(now);
26
+ return true;
27
+ }
28
+ reset() {
29
+ this.accepted = [];
30
+ }
31
+ }
5
32
  class IssueStickerHeadlessError extends Error {
6
33
  constructor(code, message, cause) {
7
34
  super(message);
@@ -68,7 +95,7 @@ class HeadlessTransport {
68
95
  const authHeader = await this.options.resolveAuthorizationHeader();
69
96
  if (authHeader) headers.Authorization = authHeader;
70
97
  }
71
- const execute = (nextHeaders) => fetch(`${getApiBaseUrl()}${endpoint}`, {
98
+ const execute = (nextHeaders) => pristineFetch(`${getApiBaseUrl()}${endpoint}`, {
72
99
  method: options.method ?? "GET",
73
100
  headers: nextHeaders,
74
101
  body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
@@ -604,7 +631,7 @@ function composeIssueFilterGroups(integrations, customFields) {
604
631
  function buildHeadlessCaptureContext({
605
632
  target,
606
633
  event,
607
- videoFile,
634
+ videoFile = null,
608
635
  createVideoPreviewUrl,
609
636
  sessionReplayEnabled
610
637
  }) {
@@ -614,7 +641,7 @@ function buildHeadlessCaptureContext({
614
641
  mediaMode,
615
642
  position,
616
643
  screenshotPreviewUrls: [],
617
- videoPreviewUrl: mediaMode === "video" && videoFile ? createVideoPreviewUrl(videoFile) : null,
644
+ videoPreviewUrl: mediaMode === "video" && videoFile && createVideoPreviewUrl ? createVideoPreviewUrl(videoFile) : null,
618
645
  targetSelector: position.selector,
619
646
  sessionReplayEnabled,
620
647
  sessionReplayPreview: null
@@ -624,6 +651,7 @@ async function captureHeadlessScreenshots({
624
651
  target,
625
652
  excludeSelectors
626
653
  }) {
654
+ const targetIsDocumentRoot = target === document.body || target === document.documentElement;
627
655
  const screenshots = await Promise.all([
628
656
  captureWithModernScreenshot({
629
657
  element: document.documentElement,
@@ -631,10 +659,12 @@ async function captureHeadlessScreenshots({
631
659
  width: window.innerWidth,
632
660
  height: window.innerHeight
633
661
  }).catch(() => void 0),
634
- captureWithModernScreenshot({
635
- element: target,
636
- excludeSelectors
637
- }).catch(() => void 0)
662
+ ...targetIsDocumentRoot ? [] : [
663
+ captureWithModernScreenshot({
664
+ element: target,
665
+ excludeSelectors
666
+ }).catch(() => void 0)
667
+ ]
638
668
  ]);
639
669
  return screenshots.filter(
640
670
  (dataUrl) => typeof dataUrl === "string" && dataUrl.length > 0
@@ -827,9 +857,7 @@ class HeadlessIssueRepository {
827
857
  this.options.getAuthMode(),
828
858
  this.options.getDefaultExternalUserId()
829
859
  );
830
- return this.request(
831
- `/api/sdk/issues/${encodeURIComponent(issueId)}${ownerQuery}`
832
- );
860
+ return this.request(`/api/sdk/issues/${encodeURIComponent(issueId)}${ownerQuery}`);
833
861
  }
834
862
  createIssue(issueData) {
835
863
  return this.request("/api/sdk/issues", {
@@ -865,73 +893,60 @@ class HeadlessIssueRepository {
865
893
  return Array.isArray(fields) ? fields : [];
866
894
  }
867
895
  /**
868
- * 이슈 저장 이후의 외부 연동(Jira/Notion/커스텀 필드)을 이어서 실행한다.
896
+ * Jira/Notion 외부 이슈 생성 페이로드를 만든다. 서버가 이슈 생성과 같은 요청
897
+ * 안에서 외부 연동을 수행하므로, 생성 요청(createIssue)에 실어 보낸다.
898
+ *
899
+ * 판정 규칙은 기존과 동일 — getActiveIntegration() 이 돌려주는 활성 연동의
900
+ * service_type 기준(jira 는 `active?.service_type === 'JIRA'`, notion 은
901
+ * `active?.service_type === 'NOTION'`). 담을 게 없으면 undefined 를 돌려줘
902
+ * 호출자가 `integrations` 키 자체를 빼게 한다.
903
+ */
904
+ async buildIntegrationsPayload(draft, schema) {
905
+ if (this.options.getAuthMode() !== "member") {
906
+ return void 0;
907
+ }
908
+ const active = await this.getActiveIntegration().catch(() => null);
909
+ const fields = (schema == null ? void 0 : schema.fields) ?? [];
910
+ return buildIssueIntegrationsPayload({
911
+ jiraEnabled: (active == null ? void 0 : active.service_type) === "JIRA",
912
+ jiraFields: getFilledDynamicValues(draft, fields, "jira"),
913
+ notionEnabled: (active == null ? void 0 : active.service_type) === "NOTION",
914
+ notionProperties: getFilledDynamicValues(draft, fields, "notion")
915
+ });
916
+ }
917
+ /**
918
+ * 이슈 저장 이후 커스텀 필드 값을 반영한다. Jira/Notion 외부 이슈 생성은
919
+ * buildIntegrationsPayload 를 통해 생성 요청 안에서 서버가 수행하므로 여기서는
920
+ * 다루지 않는다.
869
921
  *
870
922
  * 실패는 이슈 저장을 되돌리지 않으므로 throw 하지 않고 결과로 돌려준다. headless 는
871
923
  * UI 가 없어 toast 를 띄울 수 없으니, 통합하는 쪽이 이 값을 보고 판단해야 한다.
872
924
  */
873
- async syncDynamicFields(createdIssue, draft, schema, context) {
925
+ async syncDynamicFields(createdIssue, draft, schema) {
874
926
  if (this.options.getAuthMode() !== "member") {
875
927
  return { failedServices: [] };
876
928
  }
877
- const active = await this.getActiveIntegration().catch(() => null);
878
929
  const fields = (schema == null ? void 0 : schema.fields) ?? [];
879
- const jiraValues = getFilledDynamicValues(draft, fields, "jira");
880
- const notionValues = getFilledDynamicValues(draft, fields, "notion");
881
930
  const customValues = getFilledDynamicValues(draft, fields, "custom");
882
- return syncExternalIssueIntegrations({
883
- issueId: createdIssue.id,
884
- title: draft.title.trim(),
885
- description: draft.description.trim(),
886
- url: this.options.getCurrentUrl() ?? "",
887
- screenshotUrl: context.screenshotUrl,
888
- videoUrl: context.videoUrl,
889
- jira: {
890
- enabled: (active == null ? void 0 : active.service_type) === "JIRA",
891
- fields: jiraValues,
892
- createIssue: (input) => this.request("/api/jira/issues", {
893
- method: "POST",
894
- body: {
895
- projectId: this.options.getProjectId(),
896
- issueId: input.issueId,
897
- issueData: {
898
- title: input.title,
899
- description: input.description,
900
- url: input.url,
901
- screenshotUrl: input.screenshotUrl,
902
- videoUrl: input.videoUrl,
903
- fields: input.fields
931
+ let hasCustomFieldFailure = false;
932
+ await Promise.all(
933
+ Object.entries(customValues).map(
934
+ ([fieldDefinitionId, value]) => this.request(
935
+ `/api/sdk/custom-field-values/issues/${encodeURIComponent(createdIssue.id)}/values`,
936
+ {
937
+ method: "PUT",
938
+ body: {
939
+ field_definition_id: fieldDefinitionId,
940
+ value
904
941
  }
905
942
  }
943
+ ).catch((error) => {
944
+ hasCustomFieldFailure = true;
945
+ devLogger.error("headless", `custom-field 연동 실패 (이슈 저장은 성공):`, error);
906
946
  })
907
- },
908
- notion: {
909
- enabled: (active == null ? void 0 : active.service_type) === "NOTION",
910
- properties: notionValues,
911
- createPage: (input) => this.request("/api/notion/pages", {
912
- method: "POST",
913
- body: {
914
- projectId: this.options.getProjectId(),
915
- issueId: input.issueId,
916
- properties: input.properties
917
- }
918
- })
919
- },
920
- customFieldValues: customValues,
921
- upsertCustomFieldValue: (fieldDefinitionId, value) => this.request(
922
- `/api/sdk/custom-field-values/issues/${encodeURIComponent(createdIssue.id)}/values`,
923
- {
924
- method: "PUT",
925
- body: {
926
- field_definition_id: fieldDefinitionId,
927
- value
928
- }
929
- }
930
- ),
931
- onError: (service, error) => {
932
- devLogger.error("headless", `${service} 연동 실패 (이슈 저장은 성공):`, error);
933
- }
934
- });
947
+ )
948
+ );
949
+ return { failedServices: hasCustomFieldFailure ? ["custom-field"] : [] };
935
950
  }
936
951
  async fetchAllIssueStickers(filters) {
937
952
  var _a, _b, _c;
@@ -1043,14 +1058,24 @@ class HeadlessLogCollectors {
1043
1058
  this.networkCollector = null;
1044
1059
  this.consoleCollector = null;
1045
1060
  }
1061
+ /**
1062
+ * 스크린샷 캡처처럼 **우리가 서드파티 요청을 일으키는** 구간을 감싼다.
1063
+ *
1064
+ * 감싸지 않으면 스크린샷 라이브러리가 인라인하려고 끌어오는 웹폰트·이미지가 최근 50건을
1065
+ * 통째로 채워, 정작 필요한 앱 요청이 링버퍼 밖으로 밀려난다.
1066
+ */
1067
+ async pauseNetworkWhile(task) {
1068
+ const collector = this.networkCollector;
1069
+ return (collector == null ? void 0 : collector.pauseWhile) ? collector.pauseWhile(task) : task();
1070
+ }
1046
1071
  collect() {
1047
1072
  var _a, _b;
1048
- const networkLogs = filterExcludeServiceRelatedLogs(
1049
- ((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? []
1050
- ).slice(-50);
1073
+ const networkLogs = selectNetworkLogsForSubmit(
1074
+ filterExcludeServiceRelatedLogs(((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? [])
1075
+ );
1051
1076
  const consoleLogs = filterExcludeServiceRelatedConsoleLogs(
1052
1077
  ((_b = this.consoleCollector) == null ? void 0 : _b.getFilteredLogs()) ?? []
1053
- ).slice(-50);
1078
+ ).slice(-CONSOLE_LOG_SUBMIT_LIMIT);
1054
1079
  return {
1055
1080
  networkLogs: compactNetworkLogsForTransport(networkLogs) ?? [],
1056
1081
  consoleLogs
@@ -1106,6 +1131,7 @@ function createSessionReplayMeta(snapshot) {
1106
1131
  }
1107
1132
  function createInitialHeadlessSnapshot() {
1108
1133
  return {
1134
+ extensionActive: false,
1109
1135
  phase: "idle",
1110
1136
  auth: {
1111
1137
  mode: "public",
@@ -1242,10 +1268,15 @@ function buildIssueCreatePayload({
1242
1268
  currentUrl,
1243
1269
  userAgent
1244
1270
  }) {
1245
- return sanitizeString({
1271
+ var _a, _b, _c;
1272
+ const title = (_a = draft.title) == null ? void 0 : _a.trim();
1273
+ const description = (_b = draft.description) == null ? void 0 : _b.trim();
1274
+ const intent = (_c = draft.intent) == null ? void 0 : _c.trim();
1275
+ const base = {
1246
1276
  url: currentUrl,
1247
- title: draft.title.trim(),
1248
- description: draft.description.trim(),
1277
+ ...title ? { title } : {},
1278
+ ...description ? { description } : {},
1279
+ ...intent ? { intent } : {},
1249
1280
  position: draft.position,
1250
1281
  ...uploads.screenshotUploadIds.length > 0 ? { screenshot_upload_ids: uploads.screenshotUploadIds } : {},
1251
1282
  ...uploads.videoUpload ? { video_upload_id: uploads.videoUpload.id } : {},
@@ -1253,14 +1284,24 @@ function buildIssueCreatePayload({
1253
1284
  session_replay_upload_id: uploads.replayUpload.uploadId,
1254
1285
  session_replay_meta: uploads.replayUpload.meta
1255
1286
  } : {},
1256
- network_logs: logs.networkLogs.length > 0 ? logs.networkLogs : void 0,
1257
1287
  console_logs: logs.consoleLogs.length > 0 ? logs.consoleLogs : void 0,
1258
1288
  user_agent: userAgent,
1259
1289
  tenant_id: auth.tenantId,
1260
1290
  project_id: projectId,
1261
1291
  account_id: auth.accountId ?? void 0,
1262
1292
  ...buildExternalUserPayload(config.user)
1263
- });
1293
+ };
1294
+ const { networkLogs, stillOverBudget } = shrinkNetworkLogsToFit(
1295
+ logs.networkLogs,
1296
+ measureJsonBytes(base)
1297
+ );
1298
+ return {
1299
+ payload: sanitizeString({
1300
+ ...base,
1301
+ network_logs: networkLogs.length > 0 ? networkLogs : void 0
1302
+ }),
1303
+ stillOverBudget
1304
+ };
1264
1305
  }
1265
1306
  async function submitHeadlessIssueDraft({
1266
1307
  draft,
@@ -1276,12 +1317,11 @@ async function submitHeadlessIssueDraft({
1276
1317
  userAgent,
1277
1318
  uploadSessionReplaySnapshot
1278
1319
  }) {
1279
- var _a;
1280
1320
  const screenshotUploads = draft.mediaMode === "screenshot" ? await transport.uploadScreenshotPreviews(draft.screenshotPreviewUrls) : [];
1281
1321
  const screenshotUploadIds = screenshotUploads.map((upload) => upload.id);
1282
1322
  const videoUpload = draft.mediaMode === "video" && pendingVideoFile ? await transport.uploadSignedMedia(pendingVideoFile, "video") : null;
1283
1323
  const replayUpload = draft.sessionReplayEnabled ? await uploadSessionReplaySnapshot() : null;
1284
- const issueData = buildIssueCreatePayload({
1324
+ const { payload: issueData, stillOverBudget } = buildIssueCreatePayload({
1285
1325
  draft,
1286
1326
  auth,
1287
1327
  config,
@@ -1291,11 +1331,15 @@ async function submitHeadlessIssueDraft({
1291
1331
  currentUrl,
1292
1332
  userAgent
1293
1333
  });
1294
- const createdIssue = await issueRepository.createIssue(issueData);
1295
- await issueRepository.syncDynamicFields(createdIssue, draft, formSchema, {
1296
- screenshotUrl: (_a = screenshotUploads[0]) == null ? void 0 : _a.url,
1297
- videoUrl: videoUpload == null ? void 0 : videoUpload.url
1298
- });
1334
+ if (stillOverBudget) {
1335
+ throw new IssueStickerHeadlessError(
1336
+ "PAYLOAD_TOO_LARGE",
1337
+ "이슈 데이터가 전송 한도를 넘어 저장할 수 없습니다. 설명을 줄이거나 첨부를 빼고 다시 시도해주세요."
1338
+ );
1339
+ }
1340
+ const integrations = await issueRepository.buildIntegrationsPayload(draft, formSchema);
1341
+ const createdIssue = await issueRepository.createIssue({ ...issueData, integrations });
1342
+ await issueRepository.syncDynamicFields(createdIssue, draft, formSchema);
1299
1343
  return createdIssue;
1300
1344
  }
1301
1345
  const HEADLESS_CAPTURE_STYLE_ID = "issue-sticker-headless-capture-style";
@@ -1393,6 +1437,7 @@ class HeadlessClientImpl {
1393
1437
  var _a;
1394
1438
  this.config = config;
1395
1439
  this.state = new HeadlessClientState();
1440
+ this.autoIssueRateLimit = new AutoIssueRateLimit();
1396
1441
  this.pendingMedia = new HeadlessPendingMedia();
1397
1442
  this.initializePromise = null;
1398
1443
  this.captureCleanup = null;
@@ -1419,7 +1464,7 @@ class HeadlessClientImpl {
1419
1464
  var _a2;
1420
1465
  return (_a2 = this.config.user) == null ? void 0 : _a2.id;
1421
1466
  },
1422
- getCurrentUrl: () => typeof window !== "undefined" ? window.location.href : void 0
1467
+ getCurrentUrl: () => getCurrentPageUrl() || void 0
1423
1468
  });
1424
1469
  this.logCollectors = new HeadlessLogCollectors(this.config, getApiBaseUrl);
1425
1470
  this.screenRecordingController = new ScreenRecordingController({
@@ -1596,14 +1641,15 @@ class HeadlessClientImpl {
1596
1641
  this.state.setSnapshot({ phase: "idle", draft: null, formSchema: null, error: null });
1597
1642
  }
1598
1643
  async submitDraft(input) {
1644
+ var _a, _b, _c;
1599
1645
  await this.ensureInitialized();
1600
1646
  this.state.assertPhase(["draftingIssue"], "submitDraft");
1601
1647
  this.state.requireDraft();
1602
- if (!input.title.trim()) {
1603
- throw new IssueStickerHeadlessError("VALIDATION_ERROR", "제목을 입력해주세요.");
1604
- }
1605
- if (!input.description.trim()) {
1606
- throw new IssueStickerHeadlessError("VALIDATION_ERROR", "설명을 입력해주세요.");
1648
+ if (!((_a = input.title) == null ? void 0 : _a.trim()) && !((_b = input.description) == null ? void 0 : _b.trim()) && !((_c = input.intent) == null ? void 0 : _c.trim())) {
1649
+ throw new IssueStickerHeadlessError(
1650
+ "VALIDATION_ERROR",
1651
+ "제목·설명 또는 기대한 동작(intent)을 입력해주세요."
1652
+ );
1607
1653
  }
1608
1654
  this.state.setSnapshot({ phase: "submittingIssue", error: null });
1609
1655
  try {
@@ -1615,6 +1661,7 @@ class HeadlessClientImpl {
1615
1661
  screenshotPreviewUrls,
1616
1662
  title: input.title,
1617
1663
  description: input.description,
1664
+ intent: input.intent,
1618
1665
  dynamicFields: input.dynamicFields ?? {}
1619
1666
  };
1620
1667
  const createdIssue = await submitHeadlessIssueDraft({
@@ -1627,7 +1674,7 @@ class HeadlessClientImpl {
1627
1674
  config: this.config,
1628
1675
  projectId: this.state.requireProjectId(),
1629
1676
  logs: this.logCollectors.collect(),
1630
- currentUrl: typeof window !== "undefined" ? window.location.href : "",
1677
+ currentUrl: getCurrentPageUrl(),
1631
1678
  userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1632
1679
  uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot()
1633
1680
  });
@@ -1647,6 +1694,76 @@ class HeadlessClientImpl {
1647
1694
  throw nextError;
1648
1695
  }
1649
1696
  }
1697
+ async createIssue(input) {
1698
+ var _a, _b;
1699
+ try {
1700
+ if (!((_a = input.title) == null ? void 0 : _a.trim()) || !((_b = input.description) == null ? void 0 : _b.trim())) {
1701
+ return this.skipCreateIssue("제목과 설명이 모두 필요합니다.");
1702
+ }
1703
+ if (typeof document === "undefined" || typeof window === "undefined") {
1704
+ return this.skipCreateIssue("브라우저 DOM 에서만 이슈를 만들 수 있습니다.");
1705
+ }
1706
+ await this.ensureInitialized();
1707
+ if (this.snapshot.extensionActive) {
1708
+ return this.skipCreateIssue("이슈스티커 익스텐션이 이 페이지를 사용 중입니다.");
1709
+ }
1710
+ if (!this.autoIssueRateLimit.claim()) {
1711
+ return this.skipCreateIssue(
1712
+ `${this.autoIssueRateLimit.label}까지 호출할 수 있습니다. 호출 조건을 다시 확인해주세요.`
1713
+ );
1714
+ }
1715
+ const target = input.target ?? document.body;
1716
+ const sessionReplay = this.sessionReplayController.getSnapshot();
1717
+ const draft = buildHeadlessCaptureContext({
1718
+ target,
1719
+ sessionReplayEnabled: Boolean(sessionReplay.enabled && sessionReplay.canAttach)
1720
+ });
1721
+ const logs = this.logCollectors.collect();
1722
+ const screenshotPreviewUrls = input.media === "none" ? [] : await this.logCollectors.pauseNetworkWhile(
1723
+ () => captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() })
1724
+ );
1725
+ const formSchema = this.snapshot.auth.mode === "member" && Object.keys(input.dynamicFields ?? {}).length > 0 ? await this.loadFormSchema({}) : null;
1726
+ const createdIssue = await submitHeadlessIssueDraft({
1727
+ draft: {
1728
+ ...draft,
1729
+ screenshotPreviewUrls,
1730
+ title: input.title,
1731
+ description: input.description,
1732
+ dynamicFields: input.dynamicFields ?? {}
1733
+ },
1734
+ pendingVideoFile: null,
1735
+ transport: this.transport,
1736
+ issueRepository: this.issueRepository,
1737
+ formSchema,
1738
+ auth: this.snapshot.auth,
1739
+ config: this.config,
1740
+ projectId: this.state.requireProjectId(),
1741
+ logs,
1742
+ currentUrl: getCurrentPageUrl(),
1743
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1744
+ uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot()
1745
+ });
1746
+ this.state.setSnapshot({
1747
+ issues: [createdIssue, ...this.snapshot.issues]
1748
+ });
1749
+ return createdIssue;
1750
+ } catch (error) {
1751
+ return this.skipCreateIssue("이슈를 생성하지 못했습니다.", error);
1752
+ }
1753
+ }
1754
+ /**
1755
+ * createIssue 가 이슈를 만들지 않고 끝난 이유를 남긴다.
1756
+ *
1757
+ * devLogger 는 프로덕션 빌드에서 침묵하는데, 이 실패는 예외로 드러나지 않으므로
1758
+ * 통합한 쪽이 프로덕션에서도 알아야 한다. 접두어가 붙어 있어 우리 수집기가
1759
+ * 자기 로그로 걸러내므로 고객 이슈의 콘솔 로그를 오염시키지 않는다.
1760
+ */
1761
+ skipCreateIssue(reason, cause) {
1762
+ const args = [ISSUE_STICKER_INTERNAL_LOG_PREFIX, "[headless] createIssue:", reason];
1763
+ if (cause !== void 0) args.push(cause);
1764
+ console.warn(...args);
1765
+ return null;
1766
+ }
1650
1767
  async loginAsMember() {
1651
1768
  await this.ensureInitialized();
1652
1769
  if (!this.authBridge.canUseBridge) {
@@ -1685,10 +1802,15 @@ class HeadlessClientImpl {
1685
1802
  this.sessionReplayController.destroy();
1686
1803
  this.clearPendingMedia();
1687
1804
  this.logCollectors.stop();
1805
+ this.autoIssueRateLimit.reset();
1688
1806
  this.authBridge.destroy();
1689
1807
  this.state.reset();
1690
1808
  }
1691
1809
  async initializeInternal() {
1810
+ if (isIssueStickerExtensionRuntimeActive()) {
1811
+ this.state.setSnapshot({ extensionActive: true, phase: "idle", error: null });
1812
+ return;
1813
+ }
1692
1814
  this.state.setSnapshot({ phase: "initializing", error: null });
1693
1815
  try {
1694
1816
  const result = await this.request("/api/sdk/auth/public-init", {
@@ -1817,7 +1939,13 @@ class HeadlessClientImpl {
1817
1939
  const token = {};
1818
1940
  this.captureSession = {
1819
1941
  token,
1820
- screenshots: draft.mediaMode === "screenshot" ? captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() }) : null,
1942
+ screenshots: draft.mediaMode === "screenshot" ? (
1943
+ // 스크린샷은 웹폰트·이미지를 fetch 로 인라인한다. 그 사이 수집을 멈추지 않으면
1944
+ // 최근 50건이 그 요청들로 채워져 진짜 앱 요청이 밀려난다.
1945
+ this.logCollectors.pauseNetworkWhile(
1946
+ () => captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() })
1947
+ )
1948
+ ) : null,
1821
1949
  formSchema: this.loadFormSchema({})
1822
1950
  };
1823
1951
  const isCurrentSession = () => {