issue-sticker 0.3.1 → 0.4.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,7 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { memo, useRef, useState, useCallback, useEffect } from "react";
3
- import { c as createLucideIcon, u as ungzip_1, a as cn, R as Replayer } from "./createLucideIcon-CmSNiW63.js";
4
- import { T as TooltipProvider, a as Tooltip, b as TooltipTrigger, c as TooltipContent } from "./tooltip-DciWyUWs.js";
3
+ import { c as createLucideIcon, u as ungzip_1, a as cn, R as Replayer } from "./createLucideIcon-C0Ldhp96.js";
4
+ import { T as TooltipProvider, a as Tooltip, b as TooltipTrigger, c as TooltipContent } from "./tooltip-CEc_RGfI.js";
5
5
  /**
6
6
  * @license lucide-react v0.468.0 - ISC
7
7
  *
@@ -72,8 +72,107 @@ async function loadReplayEvents(url) {
72
72
  }
73
73
  return parsed;
74
74
  }
75
+ const TYPE_LABEL = {
76
+ click: "클릭",
77
+ input: "입력",
78
+ nav: "이동"
79
+ };
80
+ const NODE_TYPE_ELEMENT = 2;
81
+ const NODE_TYPE_TEXT = 3;
82
+ class NodeIndex {
83
+ constructor() {
84
+ this.nodes = /* @__PURE__ */ new Map();
85
+ }
86
+ /**
87
+ * 색인을 비우고 주어진 트리로 다시 채운다.
88
+ *
89
+ * FullSnapshot 마다 호출한다. rrweb 은 문서마다 `record()` 를 새로 돌리며 nodeId 를
90
+ * 1부터 다시 매기므로, 문서가 바뀌면 앞 문서의 색인이 남아 있으면 안 된다 — 남으면
91
+ * 같은 id 의 엉뚱한 요소 이름이 조용히 붙는다.
92
+ */
93
+ reset(root) {
94
+ this.nodes = /* @__PURE__ */ new Map();
95
+ if (root) this.add(root);
96
+ }
97
+ /** 노드와 그 하위 트리를 색인에 넣는다. `parentId` 를 주면 부모의 자식으로도 잇는다. */
98
+ add(node, parentId) {
99
+ var _a;
100
+ if (!node || typeof node.id !== "number") return;
101
+ this.nodes.set(node.id, { node, childIds: [] });
102
+ if (parentId !== void 0) {
103
+ (_a = this.nodes.get(parentId)) == null ? void 0 : _a.childIds.push(node.id);
104
+ }
105
+ for (const child of node.childNodes ?? []) {
106
+ this.add(child, node.id);
107
+ }
108
+ }
109
+ get(id) {
110
+ var _a;
111
+ return (_a = this.nodes.get(id)) == null ? void 0 : _a.node;
112
+ }
113
+ /** 직접 자식 노드들. 트리 안의 childNodes 와 mutation 으로 붙은 것을 함께 본다. */
114
+ childrenOf(id) {
115
+ const entry = this.nodes.get(id);
116
+ if (!entry) return [];
117
+ const fromTree = entry.node.childNodes ?? [];
118
+ const fromMutation = entry.childIds.map((childId) => {
119
+ var _a;
120
+ return (_a = this.nodes.get(childId)) == null ? void 0 : _a.node;
121
+ }).filter((n) => n !== void 0 && !fromTree.includes(n));
122
+ return [...fromTree, ...fromMutation];
123
+ }
124
+ /** 하위 텍스트를 모두 이어 붙인다. button·a 같은 리프 레이블용. */
125
+ deepText(id, depth = 0) {
126
+ if (depth > 10) return "";
127
+ const node = this.get(id);
128
+ if (!node) return "";
129
+ if (node.type === NODE_TYPE_TEXT) return node.textContent ?? "";
130
+ return this.childrenOf(id).map(
131
+ (child) => child.type === NODE_TYPE_TEXT ? child.textContent ?? "" : typeof child.id === "number" ? this.deepText(child.id, depth + 1) : ""
132
+ ).join("");
133
+ }
134
+ /**
135
+ * 상호작용 대상을 사람이 읽을 짧은 레이블로 바꾼다. 우선순위는 다음과 같다.
136
+ * 1. aria-label / title
137
+ * 2. form 컨트롤: placeholder → name → type (예: input[email])
138
+ * 3. 직접 자식 Text 노드만 결합 (컨테이너가 자손 텍스트를 통째로 빨아들이는 것 방지)
139
+ * 4. button / a: 하위 텍스트 전체 (리프 레이블이라 짧다)
140
+ * 5. 압축 셀렉터: tag.firstClass 또는 <tag>
141
+ *
142
+ * 40자로 자른다. 색인에 없으면 `TYPE_LABEL[type]` 폴백.
143
+ */
144
+ describe(id, type) {
145
+ const node = id === void 0 ? void 0 : this.get(id);
146
+ if (!node || node.type !== NODE_TYPE_ELEMENT || id === void 0) return TYPE_LABEL[type];
147
+ const attrs = node.attributes ?? {};
148
+ const attr = (name) => {
149
+ const value = attrs[name];
150
+ return typeof value === "string" ? value : "";
151
+ };
152
+ const ariaLabel = (attr("aria-label") || attr("title")).trim();
153
+ if (ariaLabel) return ariaLabel.slice(0, 40);
154
+ const tag = (node.tagName ?? "").toLowerCase();
155
+ if (tag === "input" || tag === "textarea" || tag === "select") {
156
+ const placeholder = tag === "select" ? "" : attr("placeholder");
157
+ const name = attr("name");
158
+ const inputType = tag === "input" ? attr("type") : "";
159
+ const label = placeholder || name || (inputType ? `${tag}[${inputType}]` : tag);
160
+ return label.trim().slice(0, 40);
161
+ }
162
+ const directText = this.childrenOf(id).filter((child) => child.type === NODE_TYPE_TEXT).map((child) => child.textContent ?? "").join("").trim().replace(/\s+/g, " ");
163
+ if (directText) return directText.slice(0, 40);
164
+ if (tag === "button" || tag === "a") {
165
+ const deep = this.deepText(id).trim().replace(/\s+/g, " ");
166
+ if (deep) return deep.slice(0, 40);
167
+ }
168
+ const firstClass = attr("class").trim().split(/\s+/)[0];
169
+ return (firstClass ? `${tag}.${firstClass}` : `<${tag}>`).slice(0, 40);
170
+ }
171
+ }
172
+ const EVENT_TYPE_FULL_SNAPSHOT = 2;
75
173
  const EVENT_TYPE_INCREMENTAL = 3;
76
174
  const EVENT_TYPE_META = 4;
175
+ const SOURCE_MUTATION$1 = 0;
77
176
  const SOURCE_MOUSE_INTERACTION = 2;
78
177
  const SOURCE_INPUT = 5;
79
178
  const MOUSE_CLICK = 2;
@@ -85,6 +184,7 @@ function deriveTimeline(events) {
85
184
  let recordedHeight = 0;
86
185
  let seenFirstMeta = false;
87
186
  const out = [];
187
+ const index = new NodeIndex();
88
188
  for (const ev of list) {
89
189
  const ts = Number((ev == null ? void 0 : ev.timestamp) ?? 0);
90
190
  const t = ts - startTime;
@@ -96,15 +196,26 @@ function deriveTimeline(events) {
96
196
  recordedHeight = Number(data.height ?? 0);
97
197
  continue;
98
198
  }
99
- out.push({ t, type: "nav", href: String(data.href ?? "") });
199
+ const href = String(data.href ?? "");
200
+ out.push({ t, type: "nav", href, label: href || TYPE_LABEL.nav });
201
+ continue;
202
+ }
203
+ if ((ev == null ? void 0 : ev.type) === EVENT_TYPE_FULL_SNAPSHOT) {
204
+ index.reset(data.node);
100
205
  continue;
101
206
  }
102
207
  if ((ev == null ? void 0 : ev.type) === EVENT_TYPE_INCREMENTAL) {
103
208
  const source = Number(data.source);
104
- if (source === SOURCE_MOUSE_INTERACTION && Number(data.type) === MOUSE_CLICK) {
105
- out.push({ t, type: "click", nodeId: Number(data.id) });
209
+ if (source === SOURCE_MUTATION$1) {
210
+ for (const add of data.adds ?? []) {
211
+ index.add(add == null ? void 0 : add.node, add == null ? void 0 : add.parentId);
212
+ }
213
+ } else if (source === SOURCE_MOUSE_INTERACTION && Number(data.type) === MOUSE_CLICK) {
214
+ const nodeId = Number(data.id);
215
+ out.push({ t, type: "click", nodeId, label: index.describe(nodeId, "click") });
106
216
  } else if (source === SOURCE_INPUT) {
107
- out.push({ t, type: "input", nodeId: Number(data.id) });
217
+ const nodeId = Number(data.id);
218
+ out.push({ t, type: "input", nodeId, label: index.describe(nodeId, "input") });
108
219
  }
109
220
  }
110
221
  }
@@ -265,38 +376,6 @@ function stripWidgetEvents(events) {
265
376
  }
266
377
  return { events: out, stripped: widgetIds.size > 0, removedEvents };
267
378
  }
268
- const TYPE_LABEL = {
269
- click: "클릭",
270
- input: "입력",
271
- nav: "이동"
272
- };
273
- function describeNode(el, type) {
274
- if (!el) return TYPE_LABEL[type];
275
- const ariaLabel = (el.getAttribute("aria-label") ?? el.getAttribute("title") ?? "").trim();
276
- if (ariaLabel) return ariaLabel.slice(0, 40);
277
- const tag = el.tagName.toLowerCase();
278
- if (tag === "input" || tag === "textarea" || tag === "select") {
279
- const placeholder = tag !== "select" ? el.placeholder ?? "" : "";
280
- const name = el.name ?? "";
281
- const inputType = tag === "input" ? el.type ?? "" : "";
282
- const label = placeholder || name || (inputType ? `${tag}[${inputType}]` : tag);
283
- return label.trim().slice(0, 40);
284
- }
285
- let directText = "";
286
- for (const child of Array.from(el.childNodes)) {
287
- if (child.nodeType === Node.TEXT_NODE) {
288
- directText += child.textContent ?? "";
289
- }
290
- }
291
- directText = directText.trim().replace(/\s+/g, " ");
292
- if (directText) return directText.slice(0, 40);
293
- if (tag === "button" || tag === "a") {
294
- const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
295
- if (text) return text.slice(0, 40);
296
- }
297
- const firstClass = el.classList.item(0);
298
- return (firstClass ? `${tag}.${firstClass}` : `<${tag}>`).slice(0, 40);
299
- }
300
379
  const MOUSE_TAIL_CONFIG = {
301
380
  lineCap: "round",
302
381
  lineWidth: 3,
@@ -315,36 +394,14 @@ function formatTime(ms) {
315
394
  const s = total % 60;
316
395
  return `${m}:${s.toString().padStart(2, "0")}`;
317
396
  }
318
- function describeEvent(mirror, ev) {
319
- if (ev.type === "nav") return ev.href || TYPE_LABEL.nav;
320
- const node = ev.nodeId != null && mirror ? mirror.getNode(ev.nodeId) : null;
321
- return describeNode(node, ev.type);
322
- }
323
- function resolveLabel(mirror, row, cache) {
324
- if (row.type === "nav") return row.href || TYPE_LABEL.nav;
325
- if (row.nodeId == null) return TYPE_LABEL[row.type];
326
- const cached = cache.get(row.nodeId);
327
- if (cached !== void 0) return cached;
328
- const label = describeEvent(mirror, row);
329
- if (label !== TYPE_LABEL[row.type]) cache.set(row.nodeId, label);
330
- return label;
331
- }
332
397
  const EventLog = memo(function EventLog2({
333
398
  rows,
334
399
  currentIndex,
335
400
  filter,
336
401
  onFilterChange,
337
402
  onSeek,
338
- total,
339
- mirror
403
+ total
340
404
  }) {
341
- const labelCacheRef = useRef(/* @__PURE__ */ new Map());
342
- const prevRowsRef = useRef(rows);
343
- if (prevRowsRef.current !== rows) {
344
- prevRowsRef.current = rows;
345
- labelCacheRef.current = /* @__PURE__ */ new Map();
346
- }
347
- const labelCache = labelCacheRef.current;
348
405
  const filtered = filter === "all" ? rows : rows.filter((r) => r.type === filter);
349
406
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 min-w-0 flex-col rounded-md border border-line-subtle bg-surface-1", children: [
350
407
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-line-subtle px-3 py-2.5", children: [
@@ -381,7 +438,7 @@ const EventLog = memo(function EventLog2({
381
438
  children: [
382
439
  /* @__PURE__ */ jsx("span", { className: "w-9 shrink-0 tabular-nums text-text-tertiary", children: formatTime(row.t) }),
383
440
  /* @__PURE__ */ jsx("span", { className: cn("h-2 w-2 shrink-0 rounded-sm", TYPE_DOT[row.type]) }),
384
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-text-secondary", children: resolveLabel(mirror, row, labelCache) }),
441
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-text-secondary", children: row.label }),
385
442
  /* @__PURE__ */ jsx("span", { className: "shrink-0 text-text-tertiary", children: TYPE_LABEL[row.type] })
386
443
  ]
387
444
  },
@@ -400,7 +457,6 @@ function SessionReplayPlayer({ url }) {
400
457
  const durationRef = useRef(0);
401
458
  const isPlayingRef = useRef(false);
402
459
  const showTrailRef = useRef(INITIAL_SHOW_TRAIL);
403
- const mirrorRef = useRef(null);
404
460
  const [state, setState] = useState("loading");
405
461
  const [isPlaying, setIsPlaying] = useState(false);
406
462
  const [showTrail, setShowTrail] = useState(INITIAL_SHOW_TRAIL);
@@ -438,7 +494,6 @@ function SessionReplayPlayer({ url }) {
438
494
  applyIsPlaying(false);
439
495
  setShowTrail(INITIAL_SHOW_TRAIL);
440
496
  showTrailRef.current = INITIAL_SHOW_TRAIL;
441
- mirrorRef.current = null;
442
497
  setCurrentMs(0);
443
498
  startedRef.current = false;
444
499
  (async () => {
@@ -463,6 +518,7 @@ function SessionReplayPlayer({ url }) {
463
518
  }
464
519
  (_a = stageRef.current) == null ? void 0 : _a.remove();
465
520
  const stage = document.createElement("div");
521
+ stage.style.pointerEvents = "none";
466
522
  if (derived.recordedWidth > 0) {
467
523
  const recW = derived.recordedWidth;
468
524
  const recH = derived.recordedHeight || derived.recordedWidth;
@@ -494,15 +550,13 @@ function SessionReplayPlayer({ url }) {
494
550
  stopRaf();
495
551
  });
496
552
  replayerRef.current = replayer;
497
- mirrorRef.current = replayer.getMirror();
498
553
  setTimeline(derived.events);
499
554
  const totalTime = replayer.getMetaData().totalTime;
500
555
  applyDuration(totalTime);
501
556
  const rows = derived.events.map((ev, i) => ({
502
557
  t: ev.t,
503
558
  type: ev.type,
504
- nodeId: ev.nodeId,
505
- href: ev.href,
559
+ label: ev.label,
506
560
  index: i
507
561
  }));
508
562
  setLogRows(rows);
@@ -596,10 +650,10 @@ function SessionReplayPlayer({ url }) {
596
650
  );
597
651
  const currentIndex = logRows.reduce((acc, row) => row.t <= currentMs ? row.index : acc, -1);
598
652
  if (state === "error") {
599
- return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "리플레이를 불러오지 못했어요." });
653
+ return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "리플레이를 불러오지 못했습니다." });
600
654
  }
601
655
  if (state === "empty") {
602
- return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "재생할 리플레이 데이터가 없어요." });
656
+ return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: "재생할 리플레이 데이터가 없습니다." });
603
657
  }
604
658
  return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(
605
659
  "div",
@@ -620,7 +674,11 @@ function SessionReplayPlayer({ url }) {
620
674
  {
621
675
  ref: hostRef,
622
676
  "data-testid": "session-replay-host",
623
- className: "overflow-hidden rounded-t-md border border-line-subtle bg-surface-2",
677
+ onClick: state === "ready" ? togglePlay : void 0,
678
+ className: cn(
679
+ "overflow-hidden rounded-t-md border border-line-subtle bg-surface-2",
680
+ state === "ready" && "cursor-pointer"
681
+ ),
624
682
  style: { position: "relative", maxHeight: "80vh" }
625
683
  }
626
684
  ),
@@ -708,8 +766,7 @@ function SessionReplayPlayer({ url }) {
708
766
  filter,
709
767
  onFilterChange: setFilter,
710
768
  onSeek: seek,
711
- total: timeline.length,
712
- mirror: mirrorRef.current
769
+ total: timeline.length
713
770
  }
714
771
  ) }) }) : null
715
772
  ]
@@ -1,7 +1,7 @@
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, z as isIssueStickerExtensionRuntimeActive } from "./externalUserPayload-BVNl_35M.js";
2
2
  import "react/jsx-runtime";
3
3
  import "react";
4
- import "./createLucideIcon-CmSNiW63.js";
4
+ import "./createLucideIcon-C0Ldhp96.js";
5
5
  class IssueStickerHeadlessError extends Error {
6
6
  constructor(code, message, cause) {
7
7
  super(message);
@@ -68,7 +68,7 @@ class HeadlessTransport {
68
68
  const authHeader = await this.options.resolveAuthorizationHeader();
69
69
  if (authHeader) headers.Authorization = authHeader;
70
70
  }
71
- const execute = (nextHeaders) => fetch(`${getApiBaseUrl()}${endpoint}`, {
71
+ const execute = (nextHeaders) => pristineFetch(`${getApiBaseUrl()}${endpoint}`, {
72
72
  method: options.method ?? "GET",
73
73
  headers: nextHeaders,
74
74
  body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
@@ -827,9 +827,7 @@ class HeadlessIssueRepository {
827
827
  this.options.getAuthMode(),
828
828
  this.options.getDefaultExternalUserId()
829
829
  );
830
- return this.request(
831
- `/api/sdk/issues/${encodeURIComponent(issueId)}${ownerQuery}`
832
- );
830
+ return this.request(`/api/sdk/issues/${encodeURIComponent(issueId)}${ownerQuery}`);
833
831
  }
834
832
  createIssue(issueData) {
835
833
  return this.request("/api/sdk/issues", {
@@ -865,73 +863,60 @@ class HeadlessIssueRepository {
865
863
  return Array.isArray(fields) ? fields : [];
866
864
  }
867
865
  /**
868
- * 이슈 저장 이후의 외부 연동(Jira/Notion/커스텀 필드)을 이어서 실행한다.
866
+ * Jira/Notion 외부 이슈 생성 페이로드를 만든다. 서버가 이슈 생성과 같은 요청
867
+ * 안에서 외부 연동을 수행하므로, 생성 요청(createIssue)에 실어 보낸다.
868
+ *
869
+ * 판정 규칙은 기존과 동일 — getActiveIntegration() 이 돌려주는 활성 연동의
870
+ * service_type 기준(jira 는 `active?.service_type === 'JIRA'`, notion 은
871
+ * `active?.service_type === 'NOTION'`). 담을 게 없으면 undefined 를 돌려줘
872
+ * 호출자가 `integrations` 키 자체를 빼게 한다.
873
+ */
874
+ async buildIntegrationsPayload(draft, schema) {
875
+ if (this.options.getAuthMode() !== "member") {
876
+ return void 0;
877
+ }
878
+ const active = await this.getActiveIntegration().catch(() => null);
879
+ const fields = (schema == null ? void 0 : schema.fields) ?? [];
880
+ return buildIssueIntegrationsPayload({
881
+ jiraEnabled: (active == null ? void 0 : active.service_type) === "JIRA",
882
+ jiraFields: getFilledDynamicValues(draft, fields, "jira"),
883
+ notionEnabled: (active == null ? void 0 : active.service_type) === "NOTION",
884
+ notionProperties: getFilledDynamicValues(draft, fields, "notion")
885
+ });
886
+ }
887
+ /**
888
+ * 이슈 저장 이후 커스텀 필드 값을 반영한다. Jira/Notion 외부 이슈 생성은
889
+ * buildIntegrationsPayload 를 통해 생성 요청 안에서 서버가 수행하므로 여기서는
890
+ * 다루지 않는다.
869
891
  *
870
892
  * 실패는 이슈 저장을 되돌리지 않으므로 throw 하지 않고 결과로 돌려준다. headless 는
871
893
  * UI 가 없어 toast 를 띄울 수 없으니, 통합하는 쪽이 이 값을 보고 판단해야 한다.
872
894
  */
873
- async syncDynamicFields(createdIssue, draft, schema, context) {
895
+ async syncDynamicFields(createdIssue, draft, schema) {
874
896
  if (this.options.getAuthMode() !== "member") {
875
897
  return { failedServices: [] };
876
898
  }
877
- const active = await this.getActiveIntegration().catch(() => null);
878
899
  const fields = (schema == null ? void 0 : schema.fields) ?? [];
879
- const jiraValues = getFilledDynamicValues(draft, fields, "jira");
880
- const notionValues = getFilledDynamicValues(draft, fields, "notion");
881
900
  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
901
+ let hasCustomFieldFailure = false;
902
+ await Promise.all(
903
+ Object.entries(customValues).map(
904
+ ([fieldDefinitionId, value]) => this.request(
905
+ `/api/sdk/custom-field-values/issues/${encodeURIComponent(createdIssue.id)}/values`,
906
+ {
907
+ method: "PUT",
908
+ body: {
909
+ field_definition_id: fieldDefinitionId,
910
+ value
904
911
  }
905
912
  }
913
+ ).catch((error) => {
914
+ hasCustomFieldFailure = true;
915
+ devLogger.error("headless", `custom-field 연동 실패 (이슈 저장은 성공):`, error);
906
916
  })
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
- });
917
+ )
918
+ );
919
+ return { failedServices: hasCustomFieldFailure ? ["custom-field"] : [] };
935
920
  }
936
921
  async fetchAllIssueStickers(filters) {
937
922
  var _a, _b, _c;
@@ -1043,14 +1028,24 @@ class HeadlessLogCollectors {
1043
1028
  this.networkCollector = null;
1044
1029
  this.consoleCollector = null;
1045
1030
  }
1031
+ /**
1032
+ * 스크린샷 캡처처럼 **우리가 서드파티 요청을 일으키는** 구간을 감싼다.
1033
+ *
1034
+ * 감싸지 않으면 스크린샷 라이브러리가 인라인하려고 끌어오는 웹폰트·이미지가 최근 50건을
1035
+ * 통째로 채워, 정작 필요한 앱 요청이 링버퍼 밖으로 밀려난다.
1036
+ */
1037
+ async pauseNetworkWhile(task) {
1038
+ const collector = this.networkCollector;
1039
+ return (collector == null ? void 0 : collector.pauseWhile) ? collector.pauseWhile(task) : task();
1040
+ }
1046
1041
  collect() {
1047
1042
  var _a, _b;
1048
- const networkLogs = filterExcludeServiceRelatedLogs(
1049
- ((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? []
1050
- ).slice(-50);
1043
+ const networkLogs = selectNetworkLogsForSubmit(
1044
+ filterExcludeServiceRelatedLogs(((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? [])
1045
+ );
1051
1046
  const consoleLogs = filterExcludeServiceRelatedConsoleLogs(
1052
1047
  ((_b = this.consoleCollector) == null ? void 0 : _b.getFilteredLogs()) ?? []
1053
- ).slice(-50);
1048
+ ).slice(-CONSOLE_LOG_SUBMIT_LIMIT);
1054
1049
  return {
1055
1050
  networkLogs: compactNetworkLogsForTransport(networkLogs) ?? [],
1056
1051
  consoleLogs
@@ -1106,6 +1101,7 @@ function createSessionReplayMeta(snapshot) {
1106
1101
  }
1107
1102
  function createInitialHeadlessSnapshot() {
1108
1103
  return {
1104
+ extensionActive: false,
1109
1105
  phase: "idle",
1110
1106
  auth: {
1111
1107
  mode: "public",
@@ -1242,7 +1238,7 @@ function buildIssueCreatePayload({
1242
1238
  currentUrl,
1243
1239
  userAgent
1244
1240
  }) {
1245
- return sanitizeString({
1241
+ const base = {
1246
1242
  url: currentUrl,
1247
1243
  title: draft.title.trim(),
1248
1244
  description: draft.description.trim(),
@@ -1253,14 +1249,24 @@ function buildIssueCreatePayload({
1253
1249
  session_replay_upload_id: uploads.replayUpload.uploadId,
1254
1250
  session_replay_meta: uploads.replayUpload.meta
1255
1251
  } : {},
1256
- network_logs: logs.networkLogs.length > 0 ? logs.networkLogs : void 0,
1257
1252
  console_logs: logs.consoleLogs.length > 0 ? logs.consoleLogs : void 0,
1258
1253
  user_agent: userAgent,
1259
1254
  tenant_id: auth.tenantId,
1260
1255
  project_id: projectId,
1261
1256
  account_id: auth.accountId ?? void 0,
1262
1257
  ...buildExternalUserPayload(config.user)
1263
- });
1258
+ };
1259
+ const { networkLogs, stillOverBudget } = shrinkNetworkLogsToFit(
1260
+ logs.networkLogs,
1261
+ measureJsonBytes(base)
1262
+ );
1263
+ return {
1264
+ payload: sanitizeString({
1265
+ ...base,
1266
+ network_logs: networkLogs.length > 0 ? networkLogs : void 0
1267
+ }),
1268
+ stillOverBudget
1269
+ };
1264
1270
  }
1265
1271
  async function submitHeadlessIssueDraft({
1266
1272
  draft,
@@ -1276,12 +1282,11 @@ async function submitHeadlessIssueDraft({
1276
1282
  userAgent,
1277
1283
  uploadSessionReplaySnapshot
1278
1284
  }) {
1279
- var _a;
1280
1285
  const screenshotUploads = draft.mediaMode === "screenshot" ? await transport.uploadScreenshotPreviews(draft.screenshotPreviewUrls) : [];
1281
1286
  const screenshotUploadIds = screenshotUploads.map((upload) => upload.id);
1282
1287
  const videoUpload = draft.mediaMode === "video" && pendingVideoFile ? await transport.uploadSignedMedia(pendingVideoFile, "video") : null;
1283
1288
  const replayUpload = draft.sessionReplayEnabled ? await uploadSessionReplaySnapshot() : null;
1284
- const issueData = buildIssueCreatePayload({
1289
+ const { payload: issueData, stillOverBudget } = buildIssueCreatePayload({
1285
1290
  draft,
1286
1291
  auth,
1287
1292
  config,
@@ -1291,11 +1296,15 @@ async function submitHeadlessIssueDraft({
1291
1296
  currentUrl,
1292
1297
  userAgent
1293
1298
  });
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
- });
1299
+ if (stillOverBudget) {
1300
+ throw new IssueStickerHeadlessError(
1301
+ "PAYLOAD_TOO_LARGE",
1302
+ "이슈 데이터가 전송 한도를 넘어 저장할 수 없습니다. 설명을 줄이거나 첨부를 빼고 다시 시도해주세요."
1303
+ );
1304
+ }
1305
+ const integrations = await issueRepository.buildIntegrationsPayload(draft, formSchema);
1306
+ const createdIssue = await issueRepository.createIssue({ ...issueData, integrations });
1307
+ await issueRepository.syncDynamicFields(createdIssue, draft, formSchema);
1299
1308
  return createdIssue;
1300
1309
  }
1301
1310
  const HEADLESS_CAPTURE_STYLE_ID = "issue-sticker-headless-capture-style";
@@ -1419,7 +1428,7 @@ class HeadlessClientImpl {
1419
1428
  var _a2;
1420
1429
  return (_a2 = this.config.user) == null ? void 0 : _a2.id;
1421
1430
  },
1422
- getCurrentUrl: () => typeof window !== "undefined" ? window.location.href : void 0
1431
+ getCurrentUrl: () => getCurrentPageUrl() || void 0
1423
1432
  });
1424
1433
  this.logCollectors = new HeadlessLogCollectors(this.config, getApiBaseUrl);
1425
1434
  this.screenRecordingController = new ScreenRecordingController({
@@ -1627,7 +1636,7 @@ class HeadlessClientImpl {
1627
1636
  config: this.config,
1628
1637
  projectId: this.state.requireProjectId(),
1629
1638
  logs: this.logCollectors.collect(),
1630
- currentUrl: typeof window !== "undefined" ? window.location.href : "",
1639
+ currentUrl: getCurrentPageUrl(),
1631
1640
  userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1632
1641
  uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot()
1633
1642
  });
@@ -1689,6 +1698,10 @@ class HeadlessClientImpl {
1689
1698
  this.state.reset();
1690
1699
  }
1691
1700
  async initializeInternal() {
1701
+ if (isIssueStickerExtensionRuntimeActive()) {
1702
+ this.state.setSnapshot({ extensionActive: true, phase: "idle", error: null });
1703
+ return;
1704
+ }
1692
1705
  this.state.setSnapshot({ phase: "initializing", error: null });
1693
1706
  try {
1694
1707
  const result = await this.request("/api/sdk/auth/public-init", {
@@ -1817,7 +1830,13 @@ class HeadlessClientImpl {
1817
1830
  const token = {};
1818
1831
  this.captureSession = {
1819
1832
  token,
1820
- screenshots: draft.mediaMode === "screenshot" ? captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() }) : null,
1833
+ screenshots: draft.mediaMode === "screenshot" ? (
1834
+ // 스크린샷은 웹폰트·이미지를 fetch 로 인라인한다. 그 사이 수집을 멈추지 않으면
1835
+ // 최근 50건이 그 요청들로 채워져 진짜 앱 요청이 밀려난다.
1836
+ this.logCollectors.pauseNetworkWhile(
1837
+ () => captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() })
1838
+ )
1839
+ ) : null,
1821
1840
  formSchema: this.loadFormSchema({})
1822
1841
  };
1823
1842
  const isCurrentSession = () => {