issue-sticker 0.11.0 → 0.12.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,8 +1,8 @@
1
- import { c as createLucideIcon, u as ungzip_1, R as REPLAY_EVENT_TYPE, C as CANVAS_INTERACTION_TAG, a as cn, b as Replayer, M as MousePointer2 } from "./three-DQdLNs7v.js";
2
- import { r } from "./three-DQdLNs7v.js";
1
+ import { c as createLucideIcon, u as ungzip_1, R as REPLAY_EVENT_TYPE, C as CANVAS_INTERACTION_TAG, a as cn, b as Replayer, M as MousePointer2 } from "./three-B228j4M6.js";
2
+ import { r } from "./three-B228j4M6.js";
3
3
  import { jsxs, jsx } from "react/jsx-runtime";
4
4
  import { memo, useRef, useEffect, useState, useCallback, useMemo } from "react";
5
- import { T as TooltipProvider, a as Tooltip, b as TooltipTrigger, c as TooltipContent } from "./tooltip-DSCAsWIp.js";
5
+ import { T as TooltipProvider, a as Tooltip, b as TooltipTrigger, c as TooltipContent } from "./tooltip-CT7e9xbU.js";
6
6
  /**
7
7
  * @license lucide-react v0.468.0 - ISC
8
8
  *
@@ -1,5 +1,5 @@
1
- import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadToSignedUrl, p as pristineFetch, g as getElementPosition, c as captureWithModernScreenshot, b as buildJiraCreateFields, e as buildNotionCreateFields, f as buildGuestOwnerQuery, h as composeIssueFilterGroups, i as buildIssueIntegrationsPayload, j as devLogger, W as WebSocketCollector, C as ConsoleCollector, N as NetworkCollector, s as selectNetworkLogsForSubmit, k as filterExcludeServiceRelatedLogs, l as filterExcludeServiceRelatedConsoleLogs, m as compactNetworkLogsForTransport, n as CONSOLE_LOG_SUBMIT_LIMIT, o as buildExternalUserPayload, q as shrinkNetworkLogsToFit, r as measureJsonBytes, t as sanitizeString, v as serializeSocketTranscript, w as uploadSocketTranscript, x as getCurrentPageUrl, S as ScreenRecordingController, y as SessionReplayCaptureController, z as preloadModernScreenshot, I as ISSUE_STICKER_INTERNAL_LOG_PREFIX } from "./externalUserPayload-BbOEmKZD.js";
2
- import { r } from "./three-DQdLNs7v.js";
1
+ import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadToSignedUrl, p as pristineFetch, g as getElementPosition, c as captureViewportWithElementCrop, b as captureWithModernScreenshot, e as buildJiraCreateFields, f as buildNotionCreateFields, h as buildGuestOwnerQuery, i as composeIssueFilterGroups, j as buildIssueIntegrationsPayload, k as devLogger, W as WebSocketCollector, C as ConsoleCollector, N as NetworkCollector, s as selectNetworkLogsForSubmit, l as filterExcludeServiceRelatedLogs, m as filterExcludeServiceRelatedConsoleLogs, n as compactNetworkLogsForTransport, o as CONSOLE_LOG_SUBMIT_LIMIT, q as buildExternalUserPayload, r as shrinkNetworkLogsToFit, t as measureJsonBytes, v as sanitizeString, w as collectAttachmentUploads, x as hasReplayUpload, y as getCurrentPageUrl, z as createIssueUploadQueues, S as ScreenRecordingController, A as SessionReplayCaptureController, B as preloadModernScreenshot, D as enqueueIssueUploads, E as normalizeUploadMimeType, I as ISSUE_STICKER_INTERNAL_LOG_PREFIX, F as toSessionReplayMeta, G as loadTusClient, H as createIndexedDbPendingUploadStore, J as ATTACHMENT_UPLOAD_DB_NAME, V as VIDEO_UPLOAD_DB_NAME, K as createMediaUploadApi } from "./externalUserPayload-DWcXyz2U.js";
2
+ import { r } from "./three-B228j4M6.js";
3
3
  import "react/jsx-runtime";
4
4
  import "react";
5
5
  const WINDOW_MS = 6e4;
@@ -30,10 +30,11 @@ class AutoIssueRateLimit {
30
30
  }
31
31
  }
32
32
  class IssueStickerHeadlessError extends Error {
33
- constructor(code, message, cause) {
33
+ constructor(code, message, cause, status) {
34
34
  super(message);
35
35
  this.code = code;
36
36
  this.cause = cause;
37
+ this.status = status;
37
38
  this.name = "IssueStickerHeadlessError";
38
39
  }
39
40
  }
@@ -117,7 +118,8 @@ class HeadlessTransport {
117
118
  throw new IssueStickerHeadlessError(
118
119
  response.status === 401 ? "MEMBER_AUTH_REQUIRED" : "NETWORK_ERROR",
119
120
  extractErrorMessage(json == null ? void 0 : json.error, (json == null ? void 0 : json.message) || `요청 실패: ${response.status}`),
120
- json
121
+ json,
122
+ response.status
121
123
  );
122
124
  }
123
125
  return json;
@@ -476,21 +478,14 @@ async function captureHeadlessScreenshots({
476
478
  excludeSelectors
477
479
  }) {
478
480
  const targetIsDocumentRoot = target === document.body || target === document.documentElement;
479
- const screenshots = await Promise.all([
480
- captureWithModernScreenshot({
481
- element: document.documentElement,
482
- excludeSelectors,
483
- width: window.innerWidth,
484
- height: window.innerHeight
485
- }).catch(() => void 0),
486
- ...targetIsDocumentRoot ? [] : [
487
- captureWithModernScreenshot({
488
- element: target,
489
- excludeSelectors
490
- }).catch(() => void 0)
491
- ]
492
- ]);
493
- return screenshots.filter(
481
+ const { fullDataUrl, elementDataUrl } = await captureViewportWithElementCrop({
482
+ element: target,
483
+ excludeSelectors
484
+ });
485
+ const elementShot = targetIsDocumentRoot || elementDataUrl ? elementDataUrl : await captureWithModernScreenshot({ element: target, excludeSelectors }).catch(
486
+ () => void 0
487
+ );
488
+ return [fullDataUrl, elementShot].filter(
494
489
  (dataUrl) => typeof dataUrl === "string" && dataUrl.length > 0
495
490
  );
496
491
  }
@@ -954,20 +949,6 @@ class HeadlessPendingMedia {
954
949
  this.file = null;
955
950
  }
956
951
  }
957
- function createReplayFile(snapshot) {
958
- return new File([snapshot.payload], "session-replay.gz", {
959
- type: "application/gzip"
960
- });
961
- }
962
- function createSessionReplayMeta(snapshot) {
963
- return {
964
- window_seconds: snapshot.windowSeconds,
965
- event_count: snapshot.eventCount,
966
- size_bytes: snapshot.sizeBytes,
967
- rrweb_version: snapshot.rrwebVersion,
968
- privacy_config_version: snapshot.privacyConfigVersion
969
- };
970
- }
971
952
  function createInitialHeadlessSnapshot() {
972
953
  return {
973
954
  phase: "idle",
@@ -993,6 +974,8 @@ function createInitialHeadlessSnapshot() {
993
974
  enabled: true,
994
975
  error: null
995
976
  },
977
+ videoUploads: [],
978
+ attachmentUploads: [],
996
979
  formSchema: null,
997
980
  filterSchema: null,
998
981
  error: null
@@ -1117,12 +1100,8 @@ function buildIssueCreatePayload({
1117
1100
  ...intent ? { intent } : {},
1118
1101
  position: draft.position,
1119
1102
  ...uploads.screenshotUploadIds.length > 0 ? { screenshot_upload_ids: uploads.screenshotUploadIds } : {},
1120
- ...uploads.videoUpload ? { video_upload_id: uploads.videoUpload.id } : {},
1121
- ...uploads.replayUpload ? {
1122
- session_replay_upload_id: uploads.replayUpload.uploadId,
1123
- session_replay_meta: uploads.replayUpload.meta
1124
- } : {},
1125
- ...uploads.socketUpload ? { socket_log_upload_id: uploads.socketUpload.uploadId } : {},
1103
+ ...uploads.videoPending ? { video_pending: true } : {},
1104
+ ...uploads.replayPending ? { session_replay_pending: true } : {},
1126
1105
  console_logs: logs.consoleLogs.length > 0 ? logs.consoleLogs : void 0,
1127
1106
  user_agent: userAgent,
1128
1107
  tenant_id: auth.tenantId,
@@ -1154,27 +1133,25 @@ async function submitHeadlessIssueDraft({
1154
1133
  logs,
1155
1134
  currentUrl,
1156
1135
  userAgent,
1157
- uploadSessionReplaySnapshot,
1136
+ takeSessionReplaySnapshot,
1158
1137
  getSocketConnections
1159
1138
  }) {
1160
1139
  const screenshotUploads = draft.mediaMode === "screenshot" ? await transport.uploadScreenshotPreviews(draft.screenshotPreviewUrls) : [];
1161
1140
  const screenshotUploadIds = screenshotUploads.map((upload) => upload.id);
1162
- const videoUpload = draft.mediaMode === "video" && pendingVideoFile ? await transport.uploadSignedMedia(pendingVideoFile, "video") : null;
1163
- const replayUpload = draft.sessionReplayEnabled ? await uploadSessionReplaySnapshot() : null;
1164
- const socketSnapshot = await serializeSocketTranscript(getSocketConnections());
1165
- const socketUpload = socketSnapshot ? await uploadSocketTranscript(
1166
- {
1167
- post: (endpoint, body) => transport.request(endpoint, { method: "POST", body }),
1168
- directUpload: directUploadToSignedUrl
1169
- },
1170
- socketSnapshot
1171
- ) : null;
1141
+ const attachments = await collectAttachmentUploads({
1142
+ takeReplay: draft.sessionReplayEnabled ? takeSessionReplaySnapshot : void 0,
1143
+ getConnections: getSocketConnections
1144
+ });
1172
1145
  const { payload: issueData, stillOverBudget } = buildIssueCreatePayload({
1173
1146
  draft,
1174
1147
  auth,
1175
1148
  config,
1176
1149
  projectId,
1177
- uploads: { screenshotUploadIds, videoUpload, replayUpload, socketUpload },
1150
+ uploads: {
1151
+ screenshotUploadIds,
1152
+ videoPending: draft.mediaMode === "video" && Boolean(pendingVideoFile),
1153
+ replayPending: hasReplayUpload(attachments)
1154
+ },
1178
1155
  logs,
1179
1156
  currentUrl,
1180
1157
  userAgent
@@ -1186,9 +1163,9 @@ async function submitHeadlessIssueDraft({
1186
1163
  );
1187
1164
  }
1188
1165
  const integrations = await issueRepository.buildIntegrationsPayload(draft, formSchema);
1189
- const createdIssue = await issueRepository.createIssue({ ...issueData, integrations });
1190
- await issueRepository.syncDynamicFields(createdIssue, draft, formSchema);
1191
- return createdIssue;
1166
+ const issue = await issueRepository.createIssue({ ...issueData, integrations });
1167
+ await issueRepository.syncDynamicFields(issue, draft, formSchema);
1168
+ return { issue, attachments };
1192
1169
  }
1193
1170
  const HEADLESS_CAPTURE_STYLE_ID = "issue-sticker-headless-capture-style";
1194
1171
  const HEADLESS_CAPTURE_STYLE = `
@@ -1280,6 +1257,21 @@ const DEFAULT_EXCLUDE_SELECTORS = [
1280
1257
  "#issue-sticker-host",
1281
1258
  '[data-issue-sticker-headless-auth="true"]'
1282
1259
  ];
1260
+ function toHeadlessVideoUpload(state) {
1261
+ return {
1262
+ issueId: state.issueId,
1263
+ status: state.status,
1264
+ progress: state.bytesTotal > 0 ? state.bytesSent / state.bytesTotal : 0
1265
+ };
1266
+ }
1267
+ function toHeadlessAttachmentUpload(state) {
1268
+ return {
1269
+ issueId: state.issueId,
1270
+ kind: state.mediaType === "socket" ? "socket" : "replay",
1271
+ status: state.status,
1272
+ progress: state.bytesTotal > 0 ? state.bytesSent / state.bytesTotal : 0
1273
+ };
1274
+ }
1283
1275
  class HeadlessClientImpl {
1284
1276
  constructor(config) {
1285
1277
  var _a;
@@ -1291,6 +1283,7 @@ class HeadlessClientImpl {
1291
1283
  this.captureCleanup = null;
1292
1284
  this.captureSession = null;
1293
1285
  this.suppressNextVideoStopSelection = false;
1286
+ this.destroyed = false;
1294
1287
  if (!((_a = config.apiKey) == null ? void 0 : _a.trim())) {
1295
1288
  throw new IssueStickerHeadlessError("VALIDATION_ERROR", "apiKey가 필요합니다.");
1296
1289
  }
@@ -1314,6 +1307,22 @@ class HeadlessClientImpl {
1314
1307
  },
1315
1308
  getCurrentUrl: () => getCurrentPageUrl() || void 0
1316
1309
  });
1310
+ this.uploadQueues = createIssueUploadQueues({
1311
+ api: createMediaUploadApi({
1312
+ post: (endpoint, body) => this.transport.request(endpoint, { method: "POST", body })
1313
+ }),
1314
+ clientInfo: "issue-sticker-sdk",
1315
+ videoStore: createIndexedDbPendingUploadStore(VIDEO_UPLOAD_DB_NAME),
1316
+ attachmentStore: createIndexedDbPendingUploadStore(ATTACHMENT_UPLOAD_DB_NAME),
1317
+ loadTus: loadTusClient,
1318
+ onEvent: (event) => event.mediaType === "video" ? void this.handleVideoUploadEvent(event) : this.handleAttachmentUploadEvent(event)
1319
+ });
1320
+ this.uploadQueues.video.subscribe((states) => {
1321
+ this.state.setSnapshot({ videoUploads: states.map(toHeadlessVideoUpload) });
1322
+ });
1323
+ this.uploadQueues.attachments.subscribe((states) => {
1324
+ this.state.setSnapshot({ attachmentUploads: states.map(toHeadlessAttachmentUpload) });
1325
+ });
1317
1326
  this.logCollectors = new HeadlessLogCollectors(this.config, getApiBaseUrl);
1318
1327
  this.screenRecordingController = new ScreenRecordingController({
1319
1328
  onStopped: (file) => {
@@ -1366,7 +1375,7 @@ class HeadlessClientImpl {
1366
1375
  this.state.setSnapshot({ phase: "loadingIssues", error: null });
1367
1376
  try {
1368
1377
  const issues = await this.issueRepository.fetchAllIssueStickers(filters);
1369
- this.state.setSnapshot({ issues, phase: "idle" });
1378
+ this.state.setSnapshot({ issues: this.keepKnownVideos(issues), phase: "idle" });
1370
1379
  } catch (error) {
1371
1380
  const nextError = this.toHeadlessError(error);
1372
1381
  this.state.setSnapshot({ phase: "idle", error: nextError });
@@ -1512,7 +1521,8 @@ class HeadlessClientImpl {
1512
1521
  intent: input.intent,
1513
1522
  dynamicFields: input.dynamicFields ?? {}
1514
1523
  };
1515
- const createdIssue = await submitHeadlessIssueDraft({
1524
+ const videoFile = submission.mediaMode === "video" ? this.pendingMedia.videoFile : null;
1525
+ const { issue: createdIssue, attachments } = await submitHeadlessIssueDraft({
1516
1526
  draft: submission,
1517
1527
  pendingVideoFile: this.pendingMedia.videoFile,
1518
1528
  transport: this.transport,
@@ -1524,9 +1534,17 @@ class HeadlessClientImpl {
1524
1534
  logs: this.logCollectors.collect(),
1525
1535
  currentUrl: getCurrentPageUrl(),
1526
1536
  userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1527
- uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot(),
1537
+ takeSessionReplaySnapshot: () => this.takeSessionReplaySnapshot(),
1528
1538
  getSocketConnections: () => this.logCollectors.getSocketConnections()
1529
1539
  });
1540
+ await enqueueIssueUploads({
1541
+ videoQueue: this.uploadQueues.video,
1542
+ attachmentQueue: this.uploadQueues.attachments,
1543
+ issueId: createdIssue.id,
1544
+ projectKey: this.state.requireProjectId(),
1545
+ video: videoFile ? { blob: videoFile, mimeType: normalizeUploadMimeType(videoFile.type, "video") } : null,
1546
+ attachments
1547
+ });
1530
1548
  this.clearCaptureListeners();
1531
1549
  this.clearPendingMedia();
1532
1550
  this.captureSession = null;
@@ -1543,6 +1561,9 @@ class HeadlessClientImpl {
1543
1561
  throw nextError;
1544
1562
  }
1545
1563
  }
1564
+ retryVideoUpload(issueId) {
1565
+ this.uploadQueues.retry(issueId);
1566
+ }
1546
1567
  async createIssue(input) {
1547
1568
  var _a, _b;
1548
1569
  try {
@@ -1569,7 +1590,7 @@ class HeadlessClientImpl {
1569
1590
  () => captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() })
1570
1591
  );
1571
1592
  const formSchema = this.snapshot.auth.mode === "member" && Object.keys(input.dynamicFields ?? {}).length > 0 ? await this.loadFormSchema({}) : null;
1572
- const createdIssue = await submitHeadlessIssueDraft({
1593
+ const { issue: createdIssue, attachments } = await submitHeadlessIssueDraft({
1573
1594
  draft: {
1574
1595
  ...draft,
1575
1596
  screenshotPreviewUrls,
@@ -1587,9 +1608,17 @@ class HeadlessClientImpl {
1587
1608
  logs,
1588
1609
  currentUrl: getCurrentPageUrl(),
1589
1610
  userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1590
- uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot(),
1611
+ takeSessionReplaySnapshot: () => this.takeSessionReplaySnapshot(),
1591
1612
  getSocketConnections: () => this.logCollectors.getSocketConnections()
1592
1613
  });
1614
+ await enqueueIssueUploads({
1615
+ videoQueue: this.uploadQueues.video,
1616
+ attachmentQueue: this.uploadQueues.attachments,
1617
+ issueId: createdIssue.id,
1618
+ projectKey: this.state.requireProjectId(),
1619
+ video: null,
1620
+ attachments
1621
+ });
1593
1622
  this.state.setSnapshot({
1594
1623
  issues: [createdIssue, ...this.snapshot.issues]
1595
1624
  });
@@ -1642,6 +1671,8 @@ class HeadlessClientImpl {
1642
1671
  void this.sessionReplayController.refresh();
1643
1672
  }
1644
1673
  destroy() {
1674
+ this.destroyed = true;
1675
+ this.uploadQueues.stop();
1645
1676
  this.clearCaptureListeners();
1646
1677
  this.suppressNextVideoStopSelection = true;
1647
1678
  this.captureSession = null;
@@ -1675,6 +1706,11 @@ class HeadlessClientImpl {
1675
1706
  filterSchema: { groups: [], unavailableReason: "public-mode" }
1676
1707
  });
1677
1708
  this.logCollectors.start();
1709
+ const { video, attachments } = this.uploadQueues;
1710
+ void this.uploadQueues.resume([result.projectId]).then(() => {
1711
+ void video.pruneExpired();
1712
+ void attachments.pruneExpired();
1713
+ });
1678
1714
  this.authBridge.mountHiddenBridgeFrame();
1679
1715
  void this.sessionReplayController.initialize();
1680
1716
  } catch (error) {
@@ -1688,27 +1724,53 @@ class HeadlessClientImpl {
1688
1724
  if (!sessionReplay.enabled || !sessionReplay.canAttach) return null;
1689
1725
  try {
1690
1726
  const snapshot = await this.sessionReplayController.takeSnapshot();
1691
- return snapshot ? createSessionReplayMeta(snapshot) : null;
1727
+ return snapshot ? toSessionReplayMeta(snapshot) : null;
1692
1728
  } catch {
1693
1729
  return null;
1694
1730
  }
1695
1731
  }
1696
- async uploadSessionReplaySnapshot() {
1732
+ async takeSessionReplaySnapshot() {
1697
1733
  const sessionReplay = this.sessionReplayController.getSnapshot();
1698
1734
  if (!sessionReplay.enabled || !sessionReplay.canAttach) return null;
1699
- try {
1700
- const snapshot = await this.sessionReplayController.takeSnapshot();
1701
- if (!snapshot) return null;
1702
- if (snapshot.eventCount <= 0) return null;
1703
- const upload = await this.transport.uploadSignedMedia(createReplayFile(snapshot), "replay");
1704
- return {
1705
- uploadId: upload.id,
1706
- url: upload.url,
1707
- meta: createSessionReplayMeta(snapshot)
1708
- };
1709
- } catch {
1710
- return null;
1711
- }
1735
+ return this.sessionReplayController.takeSnapshot().catch(() => null);
1736
+ }
1737
+ /** 스티커 목록에는 영상 URL 이 없다. 업로드 완료 때 받아 둔 URL 을 같은 이슈에 남긴다. */
1738
+ keepKnownVideos(issues) {
1739
+ const known = new Map(
1740
+ this.snapshot.issues.flatMap((issue) => issue.video ? [[issue.id, issue.video]] : [])
1741
+ );
1742
+ return issues.map(
1743
+ (issue) => !issue.video && known.has(issue.id) ? { ...issue, video: known.get(issue.id) } : issue
1744
+ );
1745
+ }
1746
+ /**
1747
+ * 업로드가 끝나면 목록·상세의 같은 이슈에 영상 상태를 반영한다. 완료는 재조회해 영상 URL 까지
1748
+ * 채우고, 재조회가 안 되면(config.user 없는 게스트 등) 상태만 고친다.
1749
+ */
1750
+ async handleVideoUploadEvent(event) {
1751
+ if (event.type === "failed" || this.destroyed) return;
1752
+ const refreshed = event.type === "completed" ? await this.issueRepository.openIssue(event.issueId).catch(() => null) : null;
1753
+ if (this.destroyed) return;
1754
+ const videoStatus = event.type === "completed" ? "ready" : "failed";
1755
+ const { issues, activeIssue } = this.snapshot;
1756
+ this.state.setSnapshot({
1757
+ issues: issues.map(
1758
+ (issue) => issue.id === event.issueId ? { ...issue, video: (refreshed == null ? void 0 : refreshed.video) ?? issue.video, video_status: videoStatus } : issue
1759
+ ),
1760
+ activeIssue: (activeIssue == null ? void 0 : activeIssue.id) === event.issueId ? refreshed ?? { ...activeIssue, video_status: videoStatus } : activeIssue
1761
+ });
1762
+ }
1763
+ /** 리플레이가 연결되거나 포기되면 목록·상세의 같은 이슈에 상태를 반영한다. 소켓 전사는 상태가 없다. */
1764
+ handleAttachmentUploadEvent(event) {
1765
+ if (event.mediaType !== "replay" || event.type === "failed" || this.destroyed) return;
1766
+ const replayStatus = event.type === "completed" ? "ready" : "failed";
1767
+ const { issues, activeIssue } = this.snapshot;
1768
+ this.state.setSnapshot({
1769
+ issues: issues.map(
1770
+ (issue) => issue.id === event.issueId ? { ...issue, replay_status: replayStatus } : issue
1771
+ ),
1772
+ activeIssue: (activeIssue == null ? void 0 : activeIssue.id) === event.issueId ? { ...activeIssue, replay_status: replayStatus } : activeIssue
1773
+ });
1712
1774
  }
1713
1775
  async handleRecordedVideoStopped(file) {
1714
1776
  if (this.suppressNextVideoStopSelection) {