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.
- package/CHANGELOG.md +29 -0
- package/README.md +22 -5
- package/dist/{createLucideIcon-CmSNiW63.js → createLucideIcon-C0Ldhp96.js} +90 -87
- package/dist/{externalUserPayload-hJjuUx6q.js → externalUserPayload-BVNl_35M.js} +2659 -1315
- package/dist/issue-sticker-headless-react.js +129 -72
- package/dist/issue-sticker-headless.js +97 -78
- package/dist/issue-sticker-react.js +2534 -2071
- package/dist/{tooltip-DciWyUWs.js → tooltip-CEc_RGfI.js} +1 -1
- package/headless.d.ts +4 -2
- package/package.json +1 -1
- package/react.d.ts +9 -1
|
@@ -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-
|
|
4
|
-
import { T as TooltipProvider, a as Tooltip, b as TooltipTrigger, c as TooltipContent } from "./tooltip-
|
|
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
|
-
|
|
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 ===
|
|
105
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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-
|
|
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) =>
|
|
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
|
-
* 이슈
|
|
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
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
909
|
-
|
|
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 =
|
|
1049
|
-
((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? []
|
|
1050
|
-
)
|
|
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(-
|
|
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
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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: () =>
|
|
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:
|
|
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" ?
|
|
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 = () => {
|