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.
- package/CHANGELOG.md +43 -0
- package/dist/_commonjsHelpers-DWwsNxpa.js +8 -0
- package/dist/{externalUserPayload-BbOEmKZD.js → externalUserPayload-DWcXyz2U.js} +1177 -326
- package/dist/index-DA6SoSp7.js +3009 -0
- package/dist/issue-sticker-headless-react.js +3 -3
- package/dist/issue-sticker-headless.js +136 -74
- package/dist/issue-sticker-react.js +1052 -683
- package/dist/{three-DQdLNs7v.js → three-B228j4M6.js} +9 -9
- package/dist/{tooltip-DSCAsWIp.js → tooltip-CT7e9xbU.js} +1 -1
- package/headless.d.ts +45 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as getCanvasSource, v as onCanvasSourceRegistered, C as CANVAS_INTERACTION_TAG, w as gzip_1, R as REPLAY_EVENT_TYPE, x as record, a as cn, p as useCallbackRef$1,
|
|
1
|
+
import { t as getCanvasSource, v as onCanvasSourceRegistered, C as CANVAS_INTERACTION_TAG, w as gzip_1, R as REPLAY_EVENT_TYPE, x as record, a as cn, p as useCallbackRef$1, k as useComposedRefs, h as Primitive, P as Presence, l as Portal$1, i as composeEventHandlers, m as createContextScope, n as createSlot, D as DismissableLayer, y as Content, z as createPopperScope, A as Anchor, f as useControllableState, B as Root2$1, g as useId, E as Arrow, d as usePortalContainer, e as OVERLAY_LAYER, s as stopEscapePropagation, o as useLayoutEffect2, V as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, F as clsx, S as Slot$2, O as OVERLAY_LAYER_INDEX, M as MousePointer2 } from "./three-B228j4M6.js";
|
|
2
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import React__default, { useState, useRef, useEffect, useCallback, createContext, useContext, useLayoutEffect, useMemo } from "react";
|
|
@@ -83,7 +83,8 @@ class ScreenRecordingController {
|
|
|
83
83
|
const mimeType = chooseMimeType(this.isTypeSupported);
|
|
84
84
|
const mediaRecorder = this.createMediaRecorder(stream, {
|
|
85
85
|
mimeType,
|
|
86
|
-
|
|
86
|
+
// 실측(VP9, 2배율, 계속 스크롤): 1.5Mbps 에서 12px 글자 손상 없음, 1.0Mbps 는 잔상.
|
|
87
|
+
videoBitsPerSecond: 15e5
|
|
87
88
|
});
|
|
88
89
|
this.mediaRecorder = mediaRecorder;
|
|
89
90
|
mediaRecorder.ondataavailable = (event) => {
|
|
@@ -894,7 +895,7 @@ function settleFrame() {
|
|
|
894
895
|
async function resolveNodeId(canvas) {
|
|
895
896
|
var _a;
|
|
896
897
|
try {
|
|
897
|
-
const { record: record2 } = await import("./three-
|
|
898
|
+
const { record: record2 } = await import("./three-B228j4M6.js").then((n) => n.I);
|
|
898
899
|
const id = (_a = record2.mirror) == null ? void 0 : _a.getId(canvas);
|
|
899
900
|
return typeof id === "number" && id > 0 ? id : null;
|
|
900
901
|
} catch {
|
|
@@ -913,7 +914,7 @@ function startCanvasInteractionTracking(options = {}) {
|
|
|
913
914
|
const lastSnapshot = /* @__PURE__ */ new WeakMap();
|
|
914
915
|
let pending = null;
|
|
915
916
|
const emit2 = options.emit ?? ((tag, payload) => {
|
|
916
|
-
void import("./three-
|
|
917
|
+
void import("./three-B228j4M6.js").then((n) => n.I).then(({ addCustomEvent }) => addCustomEvent(tag, payload));
|
|
917
918
|
});
|
|
918
919
|
const pointerOf = (started, rect) => [
|
|
919
920
|
(started.x - rect.left) / rect.width * 2 - 1,
|
|
@@ -1184,10 +1185,6 @@ function dropEventsAfter(segments, cutoff) {
|
|
|
1184
1185
|
return segments.map((s2) => ({ events: s2.events.filter((e2) => e2.timestamp <= cutoff) })).filter((s2) => s2.events.length > 0);
|
|
1185
1186
|
}
|
|
1186
1187
|
const REPLAY_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1187
|
-
async function sha256Hex$1(bytes) {
|
|
1188
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
|
|
1189
|
-
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1190
|
-
}
|
|
1191
1188
|
async function serializeSnapshot(input) {
|
|
1192
1189
|
const redact = input.redactEvents ?? ((e2) => e2);
|
|
1193
1190
|
const maxBytes = input.maxBytes ?? REPLAY_SNAPSHOT_MAX_BYTES;
|
|
@@ -1201,10 +1198,8 @@ async function serializeSnapshot(input) {
|
|
|
1201
1198
|
segments = segments.slice(1);
|
|
1202
1199
|
({ events, payload } = compress2(segments));
|
|
1203
1200
|
}
|
|
1204
|
-
const checksumSha256 = await sha256Hex$1(payload);
|
|
1205
1201
|
return {
|
|
1206
1202
|
payload,
|
|
1207
|
-
checksumSha256,
|
|
1208
1203
|
windowSeconds: spanSeconds(segments),
|
|
1209
1204
|
eventCount: events.length,
|
|
1210
1205
|
sizeBytes: payload.byteLength,
|
|
@@ -2065,8 +2060,8 @@ class WebSocketCollector {
|
|
|
2065
2060
|
}
|
|
2066
2061
|
const LOG_BACKUP_SESSION_KEY = "__issueStickerLogSession";
|
|
2067
2062
|
const DB_NAME$1 = "issue-sticker-logs";
|
|
2068
|
-
const DB_VERSION$
|
|
2069
|
-
const STORE_NAME$
|
|
2063
|
+
const DB_VERSION$2 = 1;
|
|
2064
|
+
const STORE_NAME$2 = "sessions";
|
|
2070
2065
|
const LOG_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2071
2066
|
const LOG_BACKUP_BUDGET_BYTES = 8 * 1024 * 1024;
|
|
2072
2067
|
function jsonLength(value) {
|
|
@@ -2147,12 +2142,12 @@ function requestToPromise$1(request) {
|
|
|
2147
2142
|
request.onerror = () => reject(request.error);
|
|
2148
2143
|
});
|
|
2149
2144
|
}
|
|
2150
|
-
function openDatabase$
|
|
2145
|
+
function openDatabase$2() {
|
|
2151
2146
|
return new Promise((resolve, reject) => {
|
|
2152
|
-
const request = indexedDB.open(DB_NAME$1, DB_VERSION$
|
|
2147
|
+
const request = indexedDB.open(DB_NAME$1, DB_VERSION$2);
|
|
2153
2148
|
request.onupgradeneeded = () => {
|
|
2154
|
-
if (!request.result.objectStoreNames.contains(STORE_NAME$
|
|
2155
|
-
request.result.createObjectStore(STORE_NAME$
|
|
2149
|
+
if (!request.result.objectStoreNames.contains(STORE_NAME$2)) {
|
|
2150
|
+
request.result.createObjectStore(STORE_NAME$2, { keyPath: "sessionId" });
|
|
2156
2151
|
}
|
|
2157
2152
|
};
|
|
2158
2153
|
request.onsuccess = () => {
|
|
@@ -2167,10 +2162,10 @@ function openDatabase$1() {
|
|
|
2167
2162
|
function createIndexedDbLogBackup() {
|
|
2168
2163
|
return {
|
|
2169
2164
|
async load(sessionId) {
|
|
2170
|
-
const db = await openDatabase$
|
|
2165
|
+
const db = await openDatabase$2();
|
|
2171
2166
|
try {
|
|
2172
2167
|
const record2 = await requestToPromise$1(
|
|
2173
|
-
db.transaction(STORE_NAME$
|
|
2168
|
+
db.transaction(STORE_NAME$2, "readonly").objectStore(STORE_NAME$2).get(sessionId)
|
|
2174
2169
|
);
|
|
2175
2170
|
if (!record2) return null;
|
|
2176
2171
|
if (record2.expiresAt <= Date.now()) return null;
|
|
@@ -2180,7 +2175,7 @@ function createIndexedDbLogBackup() {
|
|
|
2180
2175
|
}
|
|
2181
2176
|
},
|
|
2182
2177
|
async save(sessionId, payload) {
|
|
2183
|
-
const db = await openDatabase$
|
|
2178
|
+
const db = await openDatabase$2();
|
|
2184
2179
|
try {
|
|
2185
2180
|
const record2 = {
|
|
2186
2181
|
sessionId,
|
|
@@ -2188,16 +2183,16 @@ function createIndexedDbLogBackup() {
|
|
|
2188
2183
|
expiresAt: Date.now() + LOG_BACKUP_TTL_MS
|
|
2189
2184
|
};
|
|
2190
2185
|
await requestToPromise$1(
|
|
2191
|
-
db.transaction(STORE_NAME$
|
|
2186
|
+
db.transaction(STORE_NAME$2, "readwrite").objectStore(STORE_NAME$2).put(record2)
|
|
2192
2187
|
);
|
|
2193
2188
|
} finally {
|
|
2194
2189
|
db.close();
|
|
2195
2190
|
}
|
|
2196
2191
|
},
|
|
2197
2192
|
async prune() {
|
|
2198
|
-
const db = await openDatabase$
|
|
2193
|
+
const db = await openDatabase$2();
|
|
2199
2194
|
try {
|
|
2200
|
-
const store = db.transaction(STORE_NAME$
|
|
2195
|
+
const store = db.transaction(STORE_NAME$2, "readwrite").objectStore(STORE_NAME$2);
|
|
2201
2196
|
const records = await requestToPromise$1(store.getAll());
|
|
2202
2197
|
const now = Date.now();
|
|
2203
2198
|
const expired = records.filter((record2) => record2.expiresAt <= now);
|
|
@@ -2210,10 +2205,10 @@ function createIndexedDbLogBackup() {
|
|
|
2210
2205
|
}
|
|
2211
2206
|
},
|
|
2212
2207
|
async clear(sessionId) {
|
|
2213
|
-
const db = await openDatabase$
|
|
2208
|
+
const db = await openDatabase$2();
|
|
2214
2209
|
try {
|
|
2215
2210
|
await requestToPromise$1(
|
|
2216
|
-
db.transaction(STORE_NAME$
|
|
2211
|
+
db.transaction(STORE_NAME$2, "readwrite").objectStore(STORE_NAME$2).delete(sessionId)
|
|
2217
2212
|
);
|
|
2218
2213
|
} finally {
|
|
2219
2214
|
db.close();
|
|
@@ -2283,8 +2278,8 @@ function createSafeLogBackup(store) {
|
|
|
2283
2278
|
};
|
|
2284
2279
|
}
|
|
2285
2280
|
const DB_NAME = "issue-sticker-replay";
|
|
2286
|
-
const DB_VERSION = 1;
|
|
2287
|
-
const STORE_NAME = "segments";
|
|
2281
|
+
const DB_VERSION$1 = 1;
|
|
2282
|
+
const STORE_NAME$1 = "segments";
|
|
2288
2283
|
const REPLAY_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2289
2284
|
const REPLAY_BACKUP_SIZE_BUDGET = 32 * 1024 * 1024;
|
|
2290
2285
|
const eventSizes = /* @__PURE__ */ new WeakMap();
|
|
@@ -2336,12 +2331,12 @@ function requestToPromise(request) {
|
|
|
2336
2331
|
request.onerror = () => reject(request.error);
|
|
2337
2332
|
});
|
|
2338
2333
|
}
|
|
2339
|
-
function openDatabase() {
|
|
2334
|
+
function openDatabase$1() {
|
|
2340
2335
|
return new Promise((resolve, reject) => {
|
|
2341
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
2336
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION$1);
|
|
2342
2337
|
request.onupgradeneeded = () => {
|
|
2343
|
-
if (!request.result.objectStoreNames.contains(STORE_NAME)) {
|
|
2344
|
-
request.result.createObjectStore(STORE_NAME, { keyPath: "sessionId" });
|
|
2338
|
+
if (!request.result.objectStoreNames.contains(STORE_NAME$1)) {
|
|
2339
|
+
request.result.createObjectStore(STORE_NAME$1, { keyPath: "sessionId" });
|
|
2345
2340
|
}
|
|
2346
2341
|
};
|
|
2347
2342
|
request.onsuccess = () => {
|
|
@@ -2356,10 +2351,10 @@ function openDatabase() {
|
|
|
2356
2351
|
function createIndexedDbReplayBackup() {
|
|
2357
2352
|
return {
|
|
2358
2353
|
async load(sessionId) {
|
|
2359
|
-
const db = await openDatabase();
|
|
2354
|
+
const db = await openDatabase$1();
|
|
2360
2355
|
try {
|
|
2361
2356
|
const record2 = await requestToPromise(
|
|
2362
|
-
db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(sessionId)
|
|
2357
|
+
db.transaction(STORE_NAME$1, "readonly").objectStore(STORE_NAME$1).get(sessionId)
|
|
2363
2358
|
);
|
|
2364
2359
|
if (!record2) return null;
|
|
2365
2360
|
if (record2.expiresAt <= Date.now()) return null;
|
|
@@ -2369,7 +2364,7 @@ function createIndexedDbReplayBackup() {
|
|
|
2369
2364
|
}
|
|
2370
2365
|
},
|
|
2371
2366
|
async save(sessionId, segments) {
|
|
2372
|
-
const db = await openDatabase();
|
|
2367
|
+
const db = await openDatabase$1();
|
|
2373
2368
|
try {
|
|
2374
2369
|
const record2 = {
|
|
2375
2370
|
sessionId,
|
|
@@ -2377,16 +2372,16 @@ function createIndexedDbReplayBackup() {
|
|
|
2377
2372
|
expiresAt: Date.now() + REPLAY_BACKUP_TTL_MS
|
|
2378
2373
|
};
|
|
2379
2374
|
await requestToPromise(
|
|
2380
|
-
db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(record2)
|
|
2375
|
+
db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1).put(record2)
|
|
2381
2376
|
);
|
|
2382
2377
|
} finally {
|
|
2383
2378
|
db.close();
|
|
2384
2379
|
}
|
|
2385
2380
|
},
|
|
2386
2381
|
async prune() {
|
|
2387
|
-
const db = await openDatabase();
|
|
2382
|
+
const db = await openDatabase$1();
|
|
2388
2383
|
try {
|
|
2389
|
-
const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
|
|
2384
|
+
const store = db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1);
|
|
2390
2385
|
const records = await requestToPromise(store.getAll());
|
|
2391
2386
|
const now = Date.now();
|
|
2392
2387
|
const expired = records.filter((record2) => record2.expiresAt <= now);
|
|
@@ -2399,10 +2394,10 @@ function createIndexedDbReplayBackup() {
|
|
|
2399
2394
|
}
|
|
2400
2395
|
},
|
|
2401
2396
|
async clear(sessionId) {
|
|
2402
|
-
const db = await openDatabase();
|
|
2397
|
+
const db = await openDatabase$1();
|
|
2403
2398
|
try {
|
|
2404
2399
|
await requestToPromise(
|
|
2405
|
-
db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).delete(sessionId)
|
|
2400
|
+
db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1).delete(sessionId)
|
|
2406
2401
|
);
|
|
2407
2402
|
} finally {
|
|
2408
2403
|
db.close();
|
|
@@ -2546,6 +2541,26 @@ function createReplaySession(options = {}) {
|
|
|
2546
2541
|
}
|
|
2547
2542
|
};
|
|
2548
2543
|
}
|
|
2544
|
+
function readReleaseMarker() {
|
|
2545
|
+
try {
|
|
2546
|
+
const g = globalThis.__ISSUE_STICKER_RELEASE__;
|
|
2547
|
+
if (g && typeof g.id === "string" && g.id.length > 0) {
|
|
2548
|
+
return {
|
|
2549
|
+
release: g.id,
|
|
2550
|
+
release_source: typeof g.source === "string" && g.source.length > 0 ? g.source : "global"
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
if (typeof document !== "undefined") {
|
|
2554
|
+
const meta = document.querySelector('meta[name="issue-sticker:release"]');
|
|
2555
|
+
const content = meta == null ? void 0 : meta.getAttribute("content");
|
|
2556
|
+
if (content && content.length > 0) {
|
|
2557
|
+
return { release: content, release_source: "meta" };
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
} catch {
|
|
2561
|
+
}
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2549
2564
|
function createInitialSnapshot() {
|
|
2550
2565
|
return {
|
|
2551
2566
|
available: false,
|
|
@@ -2651,6 +2666,24 @@ class SessionReplayCaptureController {
|
|
|
2651
2666
|
this.listeners.forEach((listener) => listener(this.snapshot));
|
|
2652
2667
|
}
|
|
2653
2668
|
}
|
|
2669
|
+
function dataUrlToBlob(dataUrl) {
|
|
2670
|
+
var _a;
|
|
2671
|
+
const comma = dataUrl.indexOf(",");
|
|
2672
|
+
const header = dataUrl.slice(0, comma);
|
|
2673
|
+
const mime = ((_a = /^data:([^;,]+)/.exec(header)) == null ? void 0 : _a[1]) ?? "application/octet-stream";
|
|
2674
|
+
const binary = atob(dataUrl.slice(comma + 1));
|
|
2675
|
+
const bytes = new Uint8Array(binary.length);
|
|
2676
|
+
for (let i2 = 0; i2 < binary.length; i2 += 1) bytes[i2] = binary.charCodeAt(i2);
|
|
2677
|
+
return new Blob([bytes], { type: mime });
|
|
2678
|
+
}
|
|
2679
|
+
function blobToDataUrl(blob) {
|
|
2680
|
+
return new Promise((resolve, reject) => {
|
|
2681
|
+
const reader = new FileReader();
|
|
2682
|
+
reader.onload = () => resolve(String(reader.result));
|
|
2683
|
+
reader.onerror = () => reject(reader.error ?? new Error("스크린샷을 dataUrl 로 변환하지 못했습니다."));
|
|
2684
|
+
reader.readAsDataURL(blob);
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2654
2687
|
const SCREENSHOT_DATA_URL_QUALITY = 0.95;
|
|
2655
2688
|
let nextCaptureRunId = 0;
|
|
2656
2689
|
function createCaptureAttributeNames() {
|
|
@@ -2861,14 +2894,6 @@ function createModernStickySnapshot(root, attributeNames) {
|
|
|
2861
2894
|
}
|
|
2862
2895
|
};
|
|
2863
2896
|
}
|
|
2864
|
-
function blobToDataUrl(blob) {
|
|
2865
|
-
return new Promise((resolve, reject) => {
|
|
2866
|
-
const reader = new FileReader();
|
|
2867
|
-
reader.onload = () => resolve(String(reader.result));
|
|
2868
|
-
reader.onerror = () => reject(reader.error ?? new Error("스크린샷을 dataUrl 로 변환하지 못했습니다."));
|
|
2869
|
-
reader.readAsDataURL(blob);
|
|
2870
|
-
});
|
|
2871
|
-
}
|
|
2872
2897
|
function captureOrigin(element) {
|
|
2873
2898
|
if (element === document.documentElement || element === document.body) {
|
|
2874
2899
|
return { x: 0, y: 0 };
|
|
@@ -2920,53 +2945,836 @@ async function captureWithModernScreenshot(options) {
|
|
|
2920
2945
|
SCREENSHOT_DATA_URL_QUALITY
|
|
2921
2946
|
)
|
|
2922
2947
|
);
|
|
2923
|
-
} finally {
|
|
2924
|
-
canvasFrames.forEach((frame) => frame.bitmap.close());
|
|
2925
|
-
excludeSnapshot.cleanup();
|
|
2926
|
-
stickySnapshot.cleanup();
|
|
2948
|
+
} finally {
|
|
2949
|
+
canvasFrames.forEach((frame) => frame.bitmap.close());
|
|
2950
|
+
excludeSnapshot.cleanup();
|
|
2951
|
+
stickySnapshot.cleanup();
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
const ELEMENT_CROP_PADDING_PX = 8;
|
|
2955
|
+
const ELEMENT_CROP_QUALITY = 0.95;
|
|
2956
|
+
function measureElementCropTarget(element) {
|
|
2957
|
+
var _a;
|
|
2958
|
+
if (element.ownerDocument !== document) return null;
|
|
2959
|
+
if (element === document.documentElement || element === document.body) return null;
|
|
2960
|
+
if ((((_a = window.visualViewport) == null ? void 0 : _a.scale) ?? 1) !== 1) return null;
|
|
2961
|
+
const rect = element.getBoundingClientRect();
|
|
2962
|
+
return {
|
|
2963
|
+
rect: { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom },
|
|
2964
|
+
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
function computeElementCropRect(rect, viewport, image, padding = ELEMENT_CROP_PADDING_PX) {
|
|
2968
|
+
if (viewport.width <= 0 || viewport.height <= 0 || image.width <= 0 || image.height <= 0) {
|
|
2969
|
+
return null;
|
|
2970
|
+
}
|
|
2971
|
+
const visibleWidth = Math.min(rect.right, viewport.width) - Math.max(rect.left, 0);
|
|
2972
|
+
const visibleHeight = Math.min(rect.bottom, viewport.height) - Math.max(rect.top, 0);
|
|
2973
|
+
if (visibleWidth < 1 || visibleHeight < 1) return null;
|
|
2974
|
+
const left = Math.max(rect.left - padding, 0);
|
|
2975
|
+
const top = Math.max(rect.top - padding, 0);
|
|
2976
|
+
const right = Math.min(rect.right + padding, viewport.width);
|
|
2977
|
+
const bottom = Math.min(rect.bottom + padding, viewport.height);
|
|
2978
|
+
const kx = image.width / viewport.width;
|
|
2979
|
+
const ky = image.height / viewport.height;
|
|
2980
|
+
const sx = Math.round(left * kx);
|
|
2981
|
+
const sy = Math.round(top * ky);
|
|
2982
|
+
const sw = Math.min(Math.round(right * kx), image.width) - sx;
|
|
2983
|
+
const sh = Math.min(Math.round(bottom * ky), image.height) - sy;
|
|
2984
|
+
return sw >= 1 && sh >= 1 ? { sx, sy, sw, sh } : null;
|
|
2985
|
+
}
|
|
2986
|
+
async function cropElementFromViewportCapture(viewportDataUrl, target) {
|
|
2987
|
+
const bitmap = await createImageBitmap(dataUrlToBlob(viewportDataUrl));
|
|
2988
|
+
try {
|
|
2989
|
+
const crop = computeElementCropRect(target.rect, target.viewport, {
|
|
2990
|
+
width: bitmap.width,
|
|
2991
|
+
height: bitmap.height
|
|
2992
|
+
});
|
|
2993
|
+
if (!crop) return void 0;
|
|
2994
|
+
const canvas = new OffscreenCanvas(crop.sw, crop.sh);
|
|
2995
|
+
const context = canvas.getContext("2d");
|
|
2996
|
+
if (!context) return void 0;
|
|
2997
|
+
context.drawImage(bitmap, crop.sx, crop.sy, crop.sw, crop.sh, 0, 0, crop.sw, crop.sh);
|
|
2998
|
+
return await blobToDataUrl(
|
|
2999
|
+
await canvas.convertToBlob({ type: "image/webp", quality: ELEMENT_CROP_QUALITY })
|
|
3000
|
+
);
|
|
3001
|
+
} finally {
|
|
3002
|
+
bitmap.close();
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
async function captureViewportWithElementCrop({
|
|
3006
|
+
element,
|
|
3007
|
+
excludeSelectors = []
|
|
3008
|
+
}) {
|
|
3009
|
+
const target = measureElementCropTarget(element);
|
|
3010
|
+
const fullDataUrl = await captureWithModernScreenshot({
|
|
3011
|
+
element: document.documentElement,
|
|
3012
|
+
excludeSelectors,
|
|
3013
|
+
width: window.innerWidth,
|
|
3014
|
+
height: window.innerHeight
|
|
3015
|
+
}).catch(() => void 0);
|
|
3016
|
+
if (!fullDataUrl || !target) return { fullDataUrl };
|
|
3017
|
+
const elementDataUrl = await cropElementFromViewportCapture(fullDataUrl, target).catch(
|
|
3018
|
+
() => void 0
|
|
3019
|
+
);
|
|
3020
|
+
return { fullDataUrl, elementDataUrl };
|
|
3021
|
+
}
|
|
3022
|
+
const UPLOAD_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3023
|
+
const TUS_CHUNK_SIZE = 6 * 1024 * 1024;
|
|
3024
|
+
const TUS_RETRY_DELAYS = [0, 3e3, 5e3, 1e4, 2e4];
|
|
3025
|
+
class PermanentUploadError extends Error {
|
|
3026
|
+
}
|
|
3027
|
+
class UploadStoppedError extends Error {
|
|
3028
|
+
}
|
|
3029
|
+
const RETRYABLE_CLIENT_STATUSES = /* @__PURE__ */ new Set([401, 408, 423, 429]);
|
|
3030
|
+
function readStatus(error) {
|
|
3031
|
+
if (!error || typeof error !== "object") return void 0;
|
|
3032
|
+
const status = error.status;
|
|
3033
|
+
if (typeof status === "number") return status;
|
|
3034
|
+
const response = error.originalResponse;
|
|
3035
|
+
return typeof (response == null ? void 0 : response.getStatus) === "function" ? response.getStatus() : void 0;
|
|
3036
|
+
}
|
|
3037
|
+
function isPermanentStatus(status) {
|
|
3038
|
+
return status !== void 0 && status >= 400 && status < 500 && !RETRYABLE_CLIENT_STATUSES.has(status);
|
|
3039
|
+
}
|
|
3040
|
+
function toApiFailure(error) {
|
|
3041
|
+
return isPermanentStatus(readStatus(error)) ? new PermanentUploadError(error instanceof Error ? error.message : String(error)) : error;
|
|
3042
|
+
}
|
|
3043
|
+
const SIGNATURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
|
|
3044
|
+
function toTusFailure(error) {
|
|
3045
|
+
if (error instanceof UploadStoppedError) return error;
|
|
3046
|
+
const status = readStatus(error);
|
|
3047
|
+
if (status !== void 0 && SIGNATURE_STATUSES.has(status)) {
|
|
3048
|
+
return new PermanentUploadError(`tus ${status}`);
|
|
3049
|
+
}
|
|
3050
|
+
if (status === 409) return error;
|
|
3051
|
+
return isPermanentStatus(status) ? new PermanentUploadError(`tus ${status}`) : error;
|
|
3052
|
+
}
|
|
3053
|
+
function stripMimeParameters(mimeType) {
|
|
3054
|
+
return mimeType.split(";", 1)[0].trim().toLowerCase();
|
|
3055
|
+
}
|
|
3056
|
+
function callSafely(call) {
|
|
3057
|
+
try {
|
|
3058
|
+
call();
|
|
3059
|
+
} catch {
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
function readBlob(blob) {
|
|
3063
|
+
if (typeof blob.arrayBuffer === "function") return blob.arrayBuffer();
|
|
3064
|
+
return new Promise((resolve, reject) => {
|
|
3065
|
+
const reader = new FileReader();
|
|
3066
|
+
reader.onload = () => resolve(reader.result);
|
|
3067
|
+
reader.onerror = () => reject(reader.error ?? new Error("업로드 파일을 읽지 못했습니다."));
|
|
3068
|
+
reader.readAsArrayBuffer(blob);
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
function lockName(recordId) {
|
|
3072
|
+
return `issue-sticker-upload:${recordId}`;
|
|
3073
|
+
}
|
|
3074
|
+
function lockManager() {
|
|
3075
|
+
return typeof navigator !== "undefined" ? navigator.locks : void 0;
|
|
3076
|
+
}
|
|
3077
|
+
async function heldLockNames() {
|
|
3078
|
+
var _a;
|
|
3079
|
+
try {
|
|
3080
|
+
const snapshot = await ((_a = lockManager()) == null ? void 0 : _a.query());
|
|
3081
|
+
return new Set(((snapshot == null ? void 0 : snapshot.held) ?? []).flatMap((lock) => lock.name ? [lock.name] : []));
|
|
3082
|
+
} catch {
|
|
3083
|
+
return /* @__PURE__ */ new Set();
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
async function sha256Hex(blob) {
|
|
3087
|
+
const digest = await crypto.subtle.digest("SHA-256", await readBlob(blob));
|
|
3088
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3089
|
+
}
|
|
3090
|
+
class MediaUploadQueue {
|
|
3091
|
+
constructor(deps) {
|
|
3092
|
+
this.deps = deps;
|
|
3093
|
+
this.records = /* @__PURE__ */ new Map();
|
|
3094
|
+
this.states = /* @__PURE__ */ new Map();
|
|
3095
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
3096
|
+
this.running = false;
|
|
3097
|
+
this.stopped = false;
|
|
3098
|
+
this.cancelCurrent = null;
|
|
3099
|
+
this.finished = /* @__PURE__ */ new Set();
|
|
3100
|
+
this.failureNotified = /* @__PURE__ */ new Set();
|
|
3101
|
+
this.persisted = /* @__PURE__ */ new Set();
|
|
3102
|
+
this.held = /* @__PURE__ */ new Set();
|
|
3103
|
+
this.resolveStopped = () => void 0;
|
|
3104
|
+
this.handleOnline = () => this.requeueFailed();
|
|
3105
|
+
this.now = deps.now ?? Date.now;
|
|
3106
|
+
this.whenStopped = new Promise((resolve) => {
|
|
3107
|
+
this.resolveStopped = resolve;
|
|
3108
|
+
});
|
|
3109
|
+
if (typeof window !== "undefined") window.addEventListener("online", this.handleOnline);
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* 저장소에 넣은 뒤 resolve 한다. 저장이 실패하면 메모리로만 진행한다.
|
|
3113
|
+
* reject 하지 않는다 — 이슈는 이미 생성됐다. 기록조차 만들지 못하면 이슈 id 로 abort 해
|
|
3114
|
+
* 서버가 이슈를 바로 failed 로 내리게 한다.
|
|
3115
|
+
* 기록은 호출한 틱에 생긴다(첫 await 전). 같은 이슈의 다른 업로드와 함께 넣을 때 이 순서에 기댄다.
|
|
3116
|
+
*/
|
|
3117
|
+
async enqueue(input) {
|
|
3118
|
+
let record2;
|
|
3119
|
+
try {
|
|
3120
|
+
record2 = {
|
|
3121
|
+
id: crypto.randomUUID(),
|
|
3122
|
+
...input,
|
|
3123
|
+
mimeType: stripMimeParameters(input.mimeType),
|
|
3124
|
+
createdAt: this.now()
|
|
3125
|
+
};
|
|
3126
|
+
} catch {
|
|
3127
|
+
await this.deps.api.abortUpload({ uploadId: null, issueId: input.issueId, mediaType: input.mediaType }).catch(() => void 0);
|
|
3128
|
+
this.emit({ type: "aborted", issueId: input.issueId, mediaType: input.mediaType });
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
this.records.set(record2.id, record2);
|
|
3132
|
+
this.setState(record2, "queued", 0);
|
|
3133
|
+
await this.persist(record2);
|
|
3134
|
+
this.kick();
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* 저장소의 이 프로젝트 기록을 메모리로 가져와 붙잡아 둔다. 이미 돌고 있는 처리가 있어도
|
|
3138
|
+
* requeueFailed 가 이 프로젝트를 풀 때까지 가져온 기록은 시작하지 않는다. 새로 enqueue 한
|
|
3139
|
+
* 기록은 붙잡지 않는다.
|
|
3140
|
+
* 다른 탭이 잠금을 쥐고 올리는 기록은 가져오지도 세지도 않는다.
|
|
3141
|
+
* 새로 읽어 온 기록 중 만료되지 않은 수를 돌려준다.
|
|
3142
|
+
* 이어 올리기는 두 저장소를 모두 읽은 뒤 시작하는 `IssueUploadQueues.resume` 으로 한다.
|
|
3143
|
+
*/
|
|
3144
|
+
async load(projectKey) {
|
|
3145
|
+
const held = await heldLockNames();
|
|
3146
|
+
const stored = await this.deps.store.list().catch(() => []);
|
|
3147
|
+
let resumable = 0;
|
|
3148
|
+
for (const record2 of stored) {
|
|
3149
|
+
if (record2.projectKey !== projectKey || this.records.has(record2.id) || this.finished.has(record2.id) || held.has(lockName(record2.id))) {
|
|
3150
|
+
continue;
|
|
3151
|
+
}
|
|
3152
|
+
this.persisted.add(record2.id);
|
|
3153
|
+
const normalized = { ...record2, mediaType: record2.mediaType ?? "video" };
|
|
3154
|
+
this.records.set(normalized.id, normalized);
|
|
3155
|
+
this.held.add(normalized.id);
|
|
3156
|
+
this.setState(normalized, "queued", 0);
|
|
3157
|
+
if (!this.isExpired(normalized)) resumable += 1;
|
|
3158
|
+
}
|
|
3159
|
+
return resumable;
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* 모든 프로젝트·워크스페이스의 만료 기록을 저장소에서 지운다. 서버에는 알리지 않는다 —
|
|
3163
|
+
* 생성 24시간이 지난 업로드 중 이슈는 서버 cron 이 failed 로 내린다. 이 대기열이 들고 있는
|
|
3164
|
+
* 기록은 처리 순서에서 abort 하므로 건드리지 않는다. 지운 수를 돌려준다.
|
|
3165
|
+
*/
|
|
3166
|
+
async pruneExpired() {
|
|
3167
|
+
const stored = await this.deps.store.list().catch(() => []);
|
|
3168
|
+
const expired = stored.filter(
|
|
3169
|
+
(record2) => this.isExpired(record2) && !this.records.has(record2.id) && !this.finished.has(record2.id)
|
|
3170
|
+
);
|
|
3171
|
+
await Promise.all(
|
|
3172
|
+
expired.map((record2) => this.deps.store.delete(record2.id).catch(() => void 0))
|
|
3173
|
+
);
|
|
3174
|
+
return expired.length;
|
|
3175
|
+
}
|
|
3176
|
+
/** 그 이슈의 실패 기록을 전부 다시 올린다. 첨부 대기열은 한 이슈에 리플레이·소켓 두 기록을 둔다. */
|
|
3177
|
+
retry(issueId) {
|
|
3178
|
+
var _a;
|
|
3179
|
+
let requeued = false;
|
|
3180
|
+
for (const record2 of this.records.values()) {
|
|
3181
|
+
if (record2.issueId !== issueId || ((_a = this.states.get(record2.id)) == null ? void 0 : _a.status) !== "failed") continue;
|
|
3182
|
+
this.failureNotified.delete(record2.id);
|
|
3183
|
+
this.setState(record2, "queued", 0);
|
|
3184
|
+
requeued = true;
|
|
3185
|
+
}
|
|
3186
|
+
if (requeued) this.kick();
|
|
3187
|
+
}
|
|
3188
|
+
/** 그 이슈에 아직 처리할(queued·uploading) 기록이 있는가. 실패로 멈춘 기록은 세지 않는다. */
|
|
3189
|
+
isPending(issueId) {
|
|
3190
|
+
return [...this.records.values()].some(
|
|
3191
|
+
(record2) => {
|
|
3192
|
+
var _a;
|
|
3193
|
+
return record2.issueId === issueId && ((_a = this.states.get(record2.id)) == null ? void 0 : _a.status) !== "failed";
|
|
3194
|
+
}
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
subscribe(listener) {
|
|
3198
|
+
this.listeners.add(listener);
|
|
3199
|
+
callSafely(() => listener(this.snapshot()));
|
|
3200
|
+
return () => {
|
|
3201
|
+
this.listeners.delete(listener);
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
hasActiveUploads() {
|
|
3205
|
+
return [...this.states.values()].some((state) => state.status !== "failed");
|
|
3206
|
+
}
|
|
3207
|
+
stop() {
|
|
3208
|
+
var _a;
|
|
3209
|
+
this.stopped = true;
|
|
3210
|
+
this.resolveStopped();
|
|
3211
|
+
if (typeof window !== "undefined") window.removeEventListener("online", this.handleOnline);
|
|
3212
|
+
(_a = this.cancelCurrent) == null ? void 0 : _a.call(this);
|
|
3213
|
+
this.listeners.clear();
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* 기록을(projectKey 를 주면 그 프로젝트만) 처리 대상으로 돌린다. load 가 붙잡아 둔 기록을 풀고,
|
|
3217
|
+
* failed 로 멈춘 기록은 다시 대기시킨 뒤 처리를 시작한다.
|
|
3218
|
+
*/
|
|
3219
|
+
requeueFailed(projectKey) {
|
|
3220
|
+
var _a;
|
|
3221
|
+
for (const record2 of this.records.values()) {
|
|
3222
|
+
if (projectKey !== void 0 && record2.projectKey !== projectKey) continue;
|
|
3223
|
+
this.held.delete(record2.id);
|
|
3224
|
+
if (((_a = this.states.get(record2.id)) == null ? void 0 : _a.status) === "failed") this.setState(record2, "queued", 0);
|
|
3225
|
+
}
|
|
3226
|
+
this.kick();
|
|
3227
|
+
}
|
|
3228
|
+
kick() {
|
|
3229
|
+
if (this.running || this.stopped) return;
|
|
3230
|
+
this.running = true;
|
|
3231
|
+
void this.drain().finally(() => {
|
|
3232
|
+
this.running = false;
|
|
3233
|
+
if (this.nextQueued()) this.kick();
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
nextQueued() {
|
|
3237
|
+
return [...this.records.values()].find(
|
|
3238
|
+
(record2) => {
|
|
3239
|
+
var _a;
|
|
3240
|
+
return !this.held.has(record2.id) && ((_a = this.states.get(record2.id)) == null ? void 0 : _a.status) === "queued";
|
|
3241
|
+
}
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
async drain() {
|
|
3245
|
+
for (let record2 = this.nextQueued(); record2 && !this.stopped; record2 = this.nextQueued()) {
|
|
3246
|
+
await this.process(record2);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3250
|
+
* 저장소는 같은 사이트의 모든 탭이 함께 쓴다. 탭마다 resume 하면 같은 기록을 두 탭이 동시에
|
|
3251
|
+
* 올리게 되므로 기록 id 잠금을 쥔 탭만 처리한다. 잠금을 못 얻으면 이 탭 메모리에서만 내려놓고
|
|
3252
|
+
* 저장소 기록은 그대로 둔다. 탭이 닫히면 잠금이 풀리므로 다음 resume 에서 다시 가져간다.
|
|
3253
|
+
* navigator.locks 가 없거나 요청이 거절되는 환경(비보안 컨텍스트 등)은 잠금 없이 처리한다.
|
|
3254
|
+
*/
|
|
3255
|
+
async process(record2) {
|
|
3256
|
+
const locks = lockManager();
|
|
3257
|
+
if (!locks) return this.processRecord(record2);
|
|
3258
|
+
let entered = false;
|
|
3259
|
+
try {
|
|
3260
|
+
await locks.request(lockName(record2.id), { ifAvailable: true }, async (lock) => {
|
|
3261
|
+
entered = true;
|
|
3262
|
+
if (!lock || !await this.refresh(record2)) {
|
|
3263
|
+
this.release(record2);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
await Promise.race([this.processRecord(record2), this.whenStopped]);
|
|
3267
|
+
});
|
|
3268
|
+
} catch {
|
|
3269
|
+
if (!entered) await this.processRecord(record2);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
/**
|
|
3273
|
+
* 이 탭이 기록을 읽은 뒤 잠금을 얻기까지 다른 탭이 그 기록을 처리했을 수 있다. 저장소에서
|
|
3274
|
+
* 사라졌으면 다른 탭이 끝낸 것이라 false, 남아 있으면 그 탭이 받은 uploadId·이어 올릴 URL 을
|
|
3275
|
+
* 가져온다. 저장하지 못해 메모리에만 있는 기록과 저장소를 읽지 못한 경우는 그대로 진행한다.
|
|
3276
|
+
*/
|
|
3277
|
+
async refresh(record2) {
|
|
3278
|
+
if (!this.persisted.has(record2.id)) return true;
|
|
3279
|
+
const fresh = await this.deps.store.load(record2.id).catch(() => void 0);
|
|
3280
|
+
if (fresh === null) return false;
|
|
3281
|
+
if (fresh) Object.assign(record2, fresh, { mediaType: fresh.mediaType ?? record2.mediaType });
|
|
3282
|
+
return true;
|
|
3283
|
+
}
|
|
3284
|
+
/** 다른 탭이 맡은 기록을 이 대기열에서만 뺀다. 알림·서버 호출·저장소 삭제는 하지 않는다. */
|
|
3285
|
+
release(record2) {
|
|
3286
|
+
this.failureNotified.delete(record2.id);
|
|
3287
|
+
this.persisted.delete(record2.id);
|
|
3288
|
+
this.records.delete(record2.id);
|
|
3289
|
+
this.states.delete(record2.id);
|
|
3290
|
+
this.notify();
|
|
3291
|
+
}
|
|
3292
|
+
async processRecord(record2) {
|
|
3293
|
+
var _a;
|
|
3294
|
+
if (this.isExpired(record2)) {
|
|
3295
|
+
await this.discard(record2);
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
this.setState(record2, "uploading", 0);
|
|
3299
|
+
try {
|
|
3300
|
+
await this.ensureInitialized(record2);
|
|
3301
|
+
await this.uploadWithResign(record2);
|
|
3302
|
+
this.throwIfStopped();
|
|
3303
|
+
await this.complete(record2);
|
|
3304
|
+
} catch (error) {
|
|
3305
|
+
if (this.stopped) return;
|
|
3306
|
+
if (error instanceof PermanentUploadError) {
|
|
3307
|
+
await this.discard(record2);
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
this.setState(record2, "failed", ((_a = this.states.get(record2.id)) == null ? void 0 : _a.bytesSent) ?? 0);
|
|
3311
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return;
|
|
3312
|
+
if (!this.failureNotified.has(record2.id)) {
|
|
3313
|
+
this.failureNotified.add(record2.id);
|
|
3314
|
+
this.emit({ type: "failed", issueId: record2.issueId, mediaType: record2.mediaType });
|
|
3315
|
+
}
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
if (this.stopped) return;
|
|
3319
|
+
await this.finish(record2, "completed");
|
|
3320
|
+
}
|
|
3321
|
+
async ensureInitialized(record2) {
|
|
3322
|
+
if (record2.uploadId) return;
|
|
3323
|
+
const checksumSha256 = await sha256Hex(record2.blob).catch((error) => {
|
|
3324
|
+
throw new PermanentUploadError(error instanceof Error ? error.message : String(error));
|
|
3325
|
+
});
|
|
3326
|
+
this.throwIfStopped();
|
|
3327
|
+
const init = await this.callApi(
|
|
3328
|
+
() => this.deps.api.initUpload({
|
|
3329
|
+
mediaType: record2.mediaType,
|
|
3330
|
+
issueId: record2.issueId,
|
|
3331
|
+
mimeType: record2.mimeType,
|
|
3332
|
+
sizeBytes: record2.blob.size,
|
|
3333
|
+
checksumSha256
|
|
3334
|
+
})
|
|
3335
|
+
);
|
|
3336
|
+
Object.assign(record2, init);
|
|
3337
|
+
await this.persistProgress(record2);
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* complete 의 409 는 다른 요청(같은 기록을 이어 올린 다른 탭 등)이 먼저 연결해 상태가 바뀐 경우다.
|
|
3341
|
+
* 이미 이슈에 붙었으므로 완료로 본다.
|
|
3342
|
+
*/
|
|
3343
|
+
async complete(record2) {
|
|
3344
|
+
try {
|
|
3345
|
+
await this.deps.api.completeUpload({
|
|
3346
|
+
uploadId: record2.uploadId,
|
|
3347
|
+
issueId: record2.issueId,
|
|
3348
|
+
mediaType: record2.mediaType,
|
|
3349
|
+
replayMeta: record2.replayMeta
|
|
3350
|
+
});
|
|
3351
|
+
} catch (error) {
|
|
3352
|
+
if (readStatus(error) === 409) return;
|
|
3353
|
+
throw toApiFailure(error);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
/** stop() 뒤에는 진행 중이던 처리도 서버를 더 부르지 않는다. 기록은 저장소에 남겨 다음 resume 에 맡긴다. */
|
|
3357
|
+
throwIfStopped() {
|
|
3358
|
+
if (this.stopped) throw new UploadStoppedError();
|
|
3359
|
+
}
|
|
3360
|
+
async callApi(call) {
|
|
3361
|
+
try {
|
|
3362
|
+
return await call();
|
|
3363
|
+
} catch (error) {
|
|
3364
|
+
throw toApiFailure(error);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
/** 업로드 토큰은 2시간이면 만료된다. 400·401·403 이면 같은 path 로 다시 서명받아 한 번만 이어 간다. */
|
|
3368
|
+
async uploadWithResign(record2) {
|
|
3369
|
+
try {
|
|
3370
|
+
await this.runTus(record2);
|
|
3371
|
+
return;
|
|
3372
|
+
} catch (error) {
|
|
3373
|
+
const status = readStatus(error);
|
|
3374
|
+
if (status === void 0 || !SIGNATURE_STATUSES.has(status)) throw toTusFailure(error);
|
|
3375
|
+
}
|
|
3376
|
+
let token;
|
|
3377
|
+
try {
|
|
3378
|
+
({ token } = await this.deps.api.resignUpload(record2.uploadId));
|
|
3379
|
+
} catch (error) {
|
|
3380
|
+
if (readStatus(error) === 409) return;
|
|
3381
|
+
throw toApiFailure(error);
|
|
3382
|
+
}
|
|
3383
|
+
record2.token = token;
|
|
3384
|
+
await this.persistProgress(record2);
|
|
3385
|
+
try {
|
|
3386
|
+
await this.runTus(record2);
|
|
3387
|
+
} catch (error) {
|
|
3388
|
+
throw toTusFailure(error);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
async runTus(record2) {
|
|
3392
|
+
const { Upload } = await this.deps.loadTus();
|
|
3393
|
+
this.throwIfStopped();
|
|
3394
|
+
await new Promise((resolve, reject) => {
|
|
3395
|
+
const upload = new Upload(record2.blob, {
|
|
3396
|
+
endpoint: record2.resumableUrl,
|
|
3397
|
+
uploadUrl: record2.tusUploadUrl ?? null,
|
|
3398
|
+
chunkSize: TUS_CHUNK_SIZE,
|
|
3399
|
+
retryDelays: TUS_RETRY_DELAYS,
|
|
3400
|
+
// 이어 올릴 URL 은 우리 기록에 둔다. tus 의 localStorage 저장은 쓰지 않는다.
|
|
3401
|
+
storeFingerprintForResuming: false,
|
|
3402
|
+
headers: { "x-signature": record2.token ?? "", "x-client-info": this.deps.clientInfo },
|
|
3403
|
+
metadata: {
|
|
3404
|
+
bucketName: record2.bucket ?? "",
|
|
3405
|
+
objectName: record2.path ?? "",
|
|
3406
|
+
contentType: record2.mimeType,
|
|
3407
|
+
cacheControl: "3600"
|
|
3408
|
+
},
|
|
3409
|
+
onUploadUrlAvailable: () => {
|
|
3410
|
+
record2.tusUploadUrl = upload.url ?? void 0;
|
|
3411
|
+
void this.persistProgress(record2);
|
|
3412
|
+
},
|
|
3413
|
+
onProgress: (bytesSent, bytesTotal) => this.setState(record2, "uploading", bytesSent, bytesTotal),
|
|
3414
|
+
onSuccess: () => resolve(),
|
|
3415
|
+
onError: (error) => reject(error)
|
|
3416
|
+
});
|
|
3417
|
+
this.cancelCurrent = () => {
|
|
3418
|
+
void upload.abort(false);
|
|
3419
|
+
reject(new UploadStoppedError());
|
|
3420
|
+
};
|
|
3421
|
+
upload.start();
|
|
3422
|
+
}).finally(() => {
|
|
3423
|
+
this.cancelCurrent = null;
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
async discard(record2) {
|
|
3427
|
+
await this.deps.api.abortUpload({
|
|
3428
|
+
uploadId: record2.uploadId ?? null,
|
|
3429
|
+
issueId: record2.issueId,
|
|
3430
|
+
mediaType: record2.mediaType
|
|
3431
|
+
}).catch(() => void 0);
|
|
3432
|
+
await this.finish(record2, "aborted");
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* 끝낸 기록을 저장소에서 지운 뒤 메모리에서 빼고 알린다. 메모리에서 빼는 것과 알림 사이에
|
|
3436
|
+
* await 를 두지 않는다 — 알리미는 두 대기열의 메모리 기록으로 그 이슈가 끝났는지 보므로, 그
|
|
3437
|
+
* 사이 다른 대기열이 끝나면 양쪽이 서로 끝난 것으로 보고 따로 알린다.
|
|
3438
|
+
*/
|
|
3439
|
+
async finish(record2, type) {
|
|
3440
|
+
this.finished.add(record2.id);
|
|
3441
|
+
await this.deps.store.delete(record2.id).catch(() => void 0);
|
|
3442
|
+
this.persisted.delete(record2.id);
|
|
3443
|
+
this.failureNotified.delete(record2.id);
|
|
3444
|
+
this.records.delete(record2.id);
|
|
3445
|
+
this.states.delete(record2.id);
|
|
3446
|
+
this.notify();
|
|
3447
|
+
this.emit({ type, issueId: record2.issueId, mediaType: record2.mediaType });
|
|
3448
|
+
}
|
|
3449
|
+
async persist(record2) {
|
|
3450
|
+
await this.deps.store.save(record2).then(
|
|
3451
|
+
() => this.persisted.add(record2.id),
|
|
3452
|
+
() => void 0
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* 처리 중에 받은 결과(init·재서명·이어 올릴 URL)를 저장한다. stop() 뒤에는 쓰지 않는다 —
|
|
3457
|
+
* 잠금을 넘겨받은 새 대기열이 저장한 내용을 덮어쓰지 않게 한다.
|
|
3458
|
+
*/
|
|
3459
|
+
async persistProgress(record2) {
|
|
3460
|
+
if (this.stopped) return;
|
|
3461
|
+
await this.persist(record2);
|
|
3462
|
+
}
|
|
3463
|
+
isExpired(record2) {
|
|
3464
|
+
return this.now() - record2.createdAt >= UPLOAD_MAX_AGE_MS;
|
|
3465
|
+
}
|
|
3466
|
+
setState(record2, status, bytesSent, bytesTotal = record2.blob.size) {
|
|
3467
|
+
this.states.set(record2.id, {
|
|
3468
|
+
issueId: record2.issueId,
|
|
3469
|
+
mediaType: record2.mediaType,
|
|
3470
|
+
status,
|
|
3471
|
+
bytesSent,
|
|
3472
|
+
bytesTotal
|
|
3473
|
+
});
|
|
3474
|
+
this.notify();
|
|
3475
|
+
}
|
|
3476
|
+
snapshot() {
|
|
3477
|
+
return [...this.records.keys()].flatMap((id) => {
|
|
3478
|
+
const state = this.states.get(id);
|
|
3479
|
+
return state ? [state] : [];
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
notify() {
|
|
3483
|
+
const states = this.snapshot();
|
|
3484
|
+
this.listeners.forEach((listener) => callSafely(() => listener(states)));
|
|
3485
|
+
}
|
|
3486
|
+
emit(event) {
|
|
3487
|
+
callSafely(() => {
|
|
3488
|
+
var _a, _b;
|
|
3489
|
+
return (_b = (_a = this.deps).onEvent) == null ? void 0 : _b.call(_a, event);
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
const VIDEO_UPLOAD_DB_NAME = "issue-sticker-video-uploads";
|
|
3494
|
+
const ATTACHMENT_UPLOAD_DB_NAME = "issue-sticker-attachment-uploads";
|
|
3495
|
+
const DB_VERSION = 1;
|
|
3496
|
+
const STORE_NAME = "uploads";
|
|
3497
|
+
function openDatabase(dbName) {
|
|
3498
|
+
return new Promise((resolve, reject) => {
|
|
3499
|
+
const request = indexedDB.open(dbName, DB_VERSION);
|
|
3500
|
+
request.onupgradeneeded = () => {
|
|
3501
|
+
if (!request.result.objectStoreNames.contains(STORE_NAME)) {
|
|
3502
|
+
request.result.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
3503
|
+
}
|
|
3504
|
+
};
|
|
3505
|
+
request.onsuccess = () => {
|
|
3506
|
+
const db = request.result;
|
|
3507
|
+
db.onversionchange = () => db.close();
|
|
3508
|
+
resolve(db);
|
|
3509
|
+
};
|
|
3510
|
+
request.onerror = () => reject(request.error);
|
|
3511
|
+
request.onblocked = () => reject(new Error("indexedDB open blocked"));
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
async function withStore(dbName, mode, run) {
|
|
3515
|
+
const db = await openDatabase(dbName);
|
|
3516
|
+
try {
|
|
3517
|
+
return await new Promise((resolve, reject) => {
|
|
3518
|
+
const tx = db.transaction(STORE_NAME, mode);
|
|
3519
|
+
const request = run(tx.objectStore(STORE_NAME));
|
|
3520
|
+
tx.oncomplete = () => resolve(request.result);
|
|
3521
|
+
tx.onabort = () => reject(tx.error ?? request.error ?? new Error("indexedDB aborted"));
|
|
3522
|
+
tx.onerror = () => reject(tx.error ?? request.error);
|
|
3523
|
+
});
|
|
3524
|
+
} finally {
|
|
3525
|
+
db.close();
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
function createIndexedDbPendingUploadStore(dbName) {
|
|
3529
|
+
return {
|
|
3530
|
+
async load(id) {
|
|
3531
|
+
const found = await withStore(
|
|
3532
|
+
dbName,
|
|
3533
|
+
"readonly",
|
|
3534
|
+
(store) => store.get(id)
|
|
3535
|
+
);
|
|
3536
|
+
return found ?? null;
|
|
3537
|
+
},
|
|
3538
|
+
async save(record2) {
|
|
3539
|
+
await withStore(dbName, "readwrite", (store) => store.put(record2));
|
|
3540
|
+
},
|
|
3541
|
+
async delete(id) {
|
|
3542
|
+
await withStore(dbName, "readwrite", (store) => store.delete(id));
|
|
3543
|
+
},
|
|
3544
|
+
async list() {
|
|
3545
|
+
const all = await withStore(dbName, "readonly", (store) => store.getAll());
|
|
3546
|
+
return all.sort((a2, b) => a2.createdAt - b.createdAt);
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
const loadTusClient = () => import("./index-DA6SoSp7.js");
|
|
3551
|
+
class IssueUploadNotifier {
|
|
3552
|
+
constructor(deps) {
|
|
3553
|
+
this.deps = deps;
|
|
3554
|
+
this.outcomes = /* @__PURE__ */ new Map();
|
|
3555
|
+
}
|
|
3556
|
+
handle(event) {
|
|
3557
|
+
const notice = this.outcomes.get(event.issueId) ?? {
|
|
3558
|
+
issueId: event.issueId,
|
|
3559
|
+
completed: [],
|
|
3560
|
+
failed: [],
|
|
3561
|
+
aborted: []
|
|
3562
|
+
};
|
|
3563
|
+
notice[event.type].push(event.mediaType);
|
|
3564
|
+
this.outcomes.set(event.issueId, notice);
|
|
3565
|
+
if (this.deps.isPending(event.issueId)) return;
|
|
3566
|
+
this.outcomes.delete(event.issueId);
|
|
3567
|
+
try {
|
|
3568
|
+
this.deps.onNotice(notice);
|
|
3569
|
+
} catch {
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
function createIssueUploadQueues(deps) {
|
|
3574
|
+
const { onEvent, onNotice } = deps;
|
|
3575
|
+
const notifier = onNotice ? new IssueUploadNotifier({ isPending: (issueId) => isPending(issueId), onNotice }) : null;
|
|
3576
|
+
const create = (store) => new MediaUploadQueue({
|
|
3577
|
+
api: deps.api,
|
|
3578
|
+
store,
|
|
3579
|
+
loadTus: deps.loadTus,
|
|
3580
|
+
clientInfo: deps.clientInfo,
|
|
3581
|
+
onEvent: (event) => {
|
|
3582
|
+
onEvent == null ? void 0 : onEvent(event);
|
|
3583
|
+
notifier == null ? void 0 : notifier.handle(event);
|
|
3584
|
+
}
|
|
3585
|
+
});
|
|
3586
|
+
const video = create(deps.videoStore);
|
|
3587
|
+
const attachments = create(deps.attachmentStore);
|
|
3588
|
+
const isPending = (issueId) => video.isPending(issueId) || attachments.isPending(issueId);
|
|
3589
|
+
return {
|
|
3590
|
+
video,
|
|
3591
|
+
attachments,
|
|
3592
|
+
async resume(projectKeys) {
|
|
3593
|
+
const [videoCounts] = await Promise.all([
|
|
3594
|
+
Promise.all(projectKeys.map((key) => video.load(key))),
|
|
3595
|
+
Promise.all(projectKeys.map((key) => attachments.load(key)))
|
|
3596
|
+
]);
|
|
3597
|
+
for (const key of projectKeys) {
|
|
3598
|
+
video.requeueFailed(key);
|
|
3599
|
+
attachments.requeueFailed(key);
|
|
3600
|
+
}
|
|
3601
|
+
return videoCounts.reduce((sum, count2) => sum + count2, 0);
|
|
3602
|
+
},
|
|
3603
|
+
retry(issueId) {
|
|
3604
|
+
video.retry(issueId);
|
|
3605
|
+
attachments.retry(issueId);
|
|
3606
|
+
},
|
|
3607
|
+
stop() {
|
|
3608
|
+
video.stop();
|
|
3609
|
+
attachments.stop();
|
|
3610
|
+
},
|
|
3611
|
+
isPending
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
let nextId = 0;
|
|
3615
|
+
let toasts = [];
|
|
3616
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3617
|
+
function emit() {
|
|
3618
|
+
listeners.forEach((l2) => l2());
|
|
3619
|
+
}
|
|
3620
|
+
function addToast(message2, type, options) {
|
|
3621
|
+
const id = `toast-${++nextId}`;
|
|
3622
|
+
const t2 = {
|
|
3623
|
+
id,
|
|
3624
|
+
message: message2,
|
|
3625
|
+
type,
|
|
3626
|
+
title: options == null ? void 0 : options.title,
|
|
3627
|
+
action: options == null ? void 0 : options.action,
|
|
3628
|
+
duration: (options == null ? void 0 : options.duration) ?? 2e3,
|
|
3629
|
+
createdAt: Date.now()
|
|
3630
|
+
};
|
|
3631
|
+
toasts = [...toasts, t2];
|
|
3632
|
+
emit();
|
|
3633
|
+
return id;
|
|
3634
|
+
}
|
|
3635
|
+
function dismiss(id) {
|
|
3636
|
+
toasts = toasts.filter((t2) => t2.id !== id);
|
|
3637
|
+
emit();
|
|
3638
|
+
}
|
|
3639
|
+
function dismissAll() {
|
|
3640
|
+
toasts = [];
|
|
3641
|
+
emit();
|
|
3642
|
+
}
|
|
3643
|
+
function toast(message2, options) {
|
|
3644
|
+
return addToast(message2, "default", options);
|
|
3645
|
+
}
|
|
3646
|
+
toast.success = (message2, options) => addToast(message2, "success", options);
|
|
3647
|
+
toast.warning = (message2, options) => addToast(message2, "warning", options);
|
|
3648
|
+
toast.error = (message2, options) => addToast(message2, "error", options);
|
|
3649
|
+
toast.info = (message2, options) => addToast(message2, "info", options);
|
|
3650
|
+
toast.dismiss = dismiss;
|
|
3651
|
+
toast.dismissAll = dismissAll;
|
|
3652
|
+
function subscribe(listener) {
|
|
3653
|
+
listeners.add(listener);
|
|
3654
|
+
return () => listeners.delete(listener);
|
|
3655
|
+
}
|
|
3656
|
+
function getSnapshot() {
|
|
3657
|
+
return toasts;
|
|
3658
|
+
}
|
|
3659
|
+
function dropOldestFrames(connections, bytesToDrop) {
|
|
3660
|
+
const order = connections.flatMap(
|
|
3661
|
+
(connection, index) => connection.frames.map((frame) => {
|
|
3662
|
+
var _a;
|
|
3663
|
+
return { index, t: frame.t, bytes: ((_a = frame.data) == null ? void 0 : _a.length) ?? 0 };
|
|
3664
|
+
})
|
|
3665
|
+
).sort((a2, b) => a2.t - b.t);
|
|
3666
|
+
const dropCount = connections.map(() => 0);
|
|
3667
|
+
const dropBytes = connections.map(() => 0);
|
|
3668
|
+
let removed = 0;
|
|
3669
|
+
for (const entry of order) {
|
|
3670
|
+
if (removed >= bytesToDrop) break;
|
|
3671
|
+
dropCount[entry.index] += 1;
|
|
3672
|
+
dropBytes[entry.index] += entry.bytes;
|
|
3673
|
+
removed += entry.bytes;
|
|
3674
|
+
}
|
|
3675
|
+
return connections.map((connection, index) => {
|
|
3676
|
+
const count2 = dropCount[index];
|
|
3677
|
+
if (count2 === 0) return connection;
|
|
3678
|
+
return {
|
|
3679
|
+
...connection,
|
|
3680
|
+
frames: connection.frames.slice(count2),
|
|
3681
|
+
droppedFrames: connection.droppedFrames + count2,
|
|
3682
|
+
droppedBytes: connection.droppedBytes + dropBytes[index]
|
|
3683
|
+
};
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
const FIT_ATTEMPTS = 6;
|
|
3687
|
+
async function serializeSocketTranscript(connections, maxBytes = SOCKET_TRANSCRIPT_MAX_BYTES) {
|
|
3688
|
+
const frameCount = connections.reduce(
|
|
3689
|
+
(sum, connection) => sum + connection.frames.length + connection.droppedFrames,
|
|
3690
|
+
0
|
|
3691
|
+
);
|
|
3692
|
+
if (connections.length === 0 || frameCount === 0) return null;
|
|
3693
|
+
let current = connections;
|
|
3694
|
+
let payload = gzip_1(JSON.stringify({ connections: current }));
|
|
3695
|
+
for (let attempt = 0; attempt < FIT_ATTEMPTS && payload.byteLength > maxBytes; attempt += 1) {
|
|
3696
|
+
const storedBytes = current.reduce(
|
|
3697
|
+
(sum, connection) => sum + storedFrameBytes(connection.frames),
|
|
3698
|
+
0
|
|
3699
|
+
);
|
|
3700
|
+
if (storedBytes === 0) break;
|
|
3701
|
+
const overRatio = 1 - maxBytes / payload.byteLength;
|
|
3702
|
+
current = dropOldestFrames(current, storedBytes * Math.min(1, overRatio + 0.1));
|
|
3703
|
+
payload = gzip_1(JSON.stringify({ connections: current }));
|
|
2927
3704
|
}
|
|
3705
|
+
return {
|
|
3706
|
+
payload,
|
|
3707
|
+
sizeBytes: payload.byteLength,
|
|
3708
|
+
connectionCount: connections.length,
|
|
3709
|
+
frameCount
|
|
3710
|
+
};
|
|
2928
3711
|
}
|
|
2929
|
-
const
|
|
2930
|
-
function
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3712
|
+
const ATTACHMENT_MIME_TYPE = "application/gzip";
|
|
3713
|
+
function toSessionReplayMeta(replay) {
|
|
3714
|
+
const marker = readReleaseMarker();
|
|
3715
|
+
return {
|
|
3716
|
+
window_seconds: replay.windowSeconds,
|
|
3717
|
+
event_count: replay.eventCount,
|
|
3718
|
+
size_bytes: replay.sizeBytes,
|
|
3719
|
+
rrweb_version: replay.rrwebVersion,
|
|
3720
|
+
privacy_config_version: replay.privacyConfigVersion,
|
|
3721
|
+
...marker ? { release: marker.release, release_source: marker.release_source } : {}
|
|
3722
|
+
};
|
|
2935
3723
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
const
|
|
2939
|
-
{
|
|
2940
|
-
|
|
3724
|
+
function toAttachmentUploads(input) {
|
|
3725
|
+
const uploads = [];
|
|
3726
|
+
const { replay, socket } = input;
|
|
3727
|
+
if (replay && replay.eventCount > 0) {
|
|
3728
|
+
uploads.push({
|
|
3729
|
+
mediaType: "replay",
|
|
3730
|
+
bytes: replay.payload,
|
|
3731
|
+
mimeType: ATTACHMENT_MIME_TYPE,
|
|
3732
|
+
replayMeta: toSessionReplayMeta(replay)
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
if (socket && socket.sizeBytes <= SOCKET_TRANSCRIPT_MAX_BYTES) {
|
|
3736
|
+
uploads.push({ mediaType: "socket", bytes: socket.payload, mimeType: ATTACHMENT_MIME_TYPE });
|
|
2941
3737
|
}
|
|
3738
|
+
return uploads;
|
|
2942
3739
|
}
|
|
2943
|
-
function
|
|
2944
|
-
|
|
3740
|
+
async function collectAttachmentUploads(input) {
|
|
3741
|
+
const { takeReplay, getConnections, onError } = input;
|
|
3742
|
+
const attempt = (kind, run) => Promise.resolve().then(run).catch((error) => {
|
|
3743
|
+
onError == null ? void 0 : onError(kind, error);
|
|
3744
|
+
return null;
|
|
3745
|
+
});
|
|
3746
|
+
const replay = takeReplay ? await attempt("replay", takeReplay) : null;
|
|
3747
|
+
const socket = await attempt(
|
|
3748
|
+
"socket",
|
|
3749
|
+
async () => serializeSocketTranscript(await getConnections())
|
|
3750
|
+
);
|
|
3751
|
+
return toAttachmentUploads({ replay, socket });
|
|
2945
3752
|
}
|
|
2946
|
-
function
|
|
2947
|
-
|
|
2948
|
-
return;
|
|
2949
|
-
}
|
|
2950
|
-
const log = console[method];
|
|
2951
|
-
log(ISSUE_STICKER_INTERNAL_LOG_PREFIX, `[${scope}]`, ...args);
|
|
3753
|
+
function hasReplayUpload(uploads) {
|
|
3754
|
+
return uploads.some((upload) => upload.mediaType === "replay");
|
|
2952
3755
|
}
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
warn(scope, ...args) {
|
|
2961
|
-
write("warn", scope, args);
|
|
2962
|
-
},
|
|
2963
|
-
error(scope, ...args) {
|
|
2964
|
-
write("error", scope, args);
|
|
2965
|
-
},
|
|
2966
|
-
debug(scope, ...args) {
|
|
2967
|
-
write("debug", scope, args);
|
|
3756
|
+
async function enqueueIssueUploads(input) {
|
|
3757
|
+
const { issueId, projectKey } = input;
|
|
3758
|
+
const pending = [];
|
|
3759
|
+
if (input.video) {
|
|
3760
|
+
pending.push(
|
|
3761
|
+
input.videoQueue.enqueue({ issueId, projectKey, mediaType: "video", ...input.video })
|
|
3762
|
+
);
|
|
2968
3763
|
}
|
|
2969
|
-
|
|
3764
|
+
for (const attachment of input.attachments) {
|
|
3765
|
+
pending.push(
|
|
3766
|
+
input.attachmentQueue.enqueue({
|
|
3767
|
+
issueId,
|
|
3768
|
+
projectKey,
|
|
3769
|
+
mediaType: attachment.mediaType,
|
|
3770
|
+
blob: new Blob([attachment.bytes], { type: attachment.mimeType }),
|
|
3771
|
+
mimeType: attachment.mimeType,
|
|
3772
|
+
replayMeta: attachment.replayMeta
|
|
3773
|
+
})
|
|
3774
|
+
);
|
|
3775
|
+
}
|
|
3776
|
+
await Promise.all(pending);
|
|
3777
|
+
}
|
|
2970
3778
|
class DirectUploadError extends Error {
|
|
2971
3779
|
constructor(message2, status, data) {
|
|
2972
3780
|
super(message2);
|
|
@@ -3014,7 +3822,8 @@ async function directUploadToSignedUrl(signedUrl, file, contentType) {
|
|
|
3014
3822
|
const response = await fetch(signedUrl, {
|
|
3015
3823
|
method: "PUT",
|
|
3016
3824
|
headers: {
|
|
3017
|
-
"Content-Type": contentType
|
|
3825
|
+
"Content-Type": contentType,
|
|
3826
|
+
"x-client-info": "issue-sticker-sdk"
|
|
3018
3827
|
},
|
|
3019
3828
|
body: file
|
|
3020
3829
|
});
|
|
@@ -3066,6 +3875,83 @@ async function uploadSignedMediaWithApi(api, file, mediaType, options = {}) {
|
|
|
3066
3875
|
url: publicUrl
|
|
3067
3876
|
};
|
|
3068
3877
|
}
|
|
3878
|
+
function createMediaUploadApi(api) {
|
|
3879
|
+
return {
|
|
3880
|
+
async initUpload({ mediaType, issueId, mimeType, sizeBytes, checksumSha256 }) {
|
|
3881
|
+
const { uploads } = await api.post(
|
|
3882
|
+
"/api/media/uploads/init",
|
|
3883
|
+
{ files: [{ mediaType, mimeType, sizeBytes, checksumSha256, issueId }] }
|
|
3884
|
+
);
|
|
3885
|
+
const upload = uploads[0];
|
|
3886
|
+
if (!(upload == null ? void 0 : upload.id) || !upload.resumableUrl) {
|
|
3887
|
+
throw new Error("업로드 URL 발급 응답이 올바르지 않습니다.");
|
|
3888
|
+
}
|
|
3889
|
+
return {
|
|
3890
|
+
uploadId: upload.id,
|
|
3891
|
+
bucket: upload.bucket,
|
|
3892
|
+
path: upload.path,
|
|
3893
|
+
token: upload.token,
|
|
3894
|
+
resumableUrl: upload.resumableUrl
|
|
3895
|
+
};
|
|
3896
|
+
},
|
|
3897
|
+
resignUpload: (uploadId) => api.post(`/api/media/uploads/${encodeURIComponent(uploadId)}/resign`),
|
|
3898
|
+
async completeUpload({ uploadId, issueId, replayMeta }) {
|
|
3899
|
+
await api.post("/api/media/uploads/complete", {
|
|
3900
|
+
uploadIds: [uploadId],
|
|
3901
|
+
issueId,
|
|
3902
|
+
...replayMeta ? { replayMeta } : {}
|
|
3903
|
+
});
|
|
3904
|
+
},
|
|
3905
|
+
async abortUpload({ uploadId, issueId, mediaType }) {
|
|
3906
|
+
await api.post("/api/media/uploads/abort", {
|
|
3907
|
+
issueId,
|
|
3908
|
+
mediaType,
|
|
3909
|
+
...uploadId ? { uploadId } : {}
|
|
3910
|
+
});
|
|
3911
|
+
}
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
const ISSUE_STICKER_INTERNAL_LOG_PREFIX = "__ISSUE_STICKER_INTERNAL_DEBUG__";
|
|
3915
|
+
function isInternalDebugLog$1(message2) {
|
|
3916
|
+
return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
|
|
3917
|
+
}
|
|
3918
|
+
function isIssueStickerInternalConsoleLog$1(log) {
|
|
3919
|
+
return isInternalDebugLog$1(log.message);
|
|
3920
|
+
}
|
|
3921
|
+
const __vite_import_meta_env__ = { "DEV": false };
|
|
3922
|
+
function isDevLoggingEnabled() {
|
|
3923
|
+
const env = __vite_import_meta_env__;
|
|
3924
|
+
{
|
|
3925
|
+
return env.DEV;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
function isInternalDebugLog(message2) {
|
|
3929
|
+
return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
|
|
3930
|
+
}
|
|
3931
|
+
function write(method, scope, args) {
|
|
3932
|
+
if (!isDevLoggingEnabled()) {
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
const log = console[method];
|
|
3936
|
+
log(ISSUE_STICKER_INTERNAL_LOG_PREFIX, `[${scope}]`, ...args);
|
|
3937
|
+
}
|
|
3938
|
+
const devLogger = {
|
|
3939
|
+
log(scope, ...args) {
|
|
3940
|
+
write("log", scope, args);
|
|
3941
|
+
},
|
|
3942
|
+
info(scope, ...args) {
|
|
3943
|
+
write("info", scope, args);
|
|
3944
|
+
},
|
|
3945
|
+
warn(scope, ...args) {
|
|
3946
|
+
write("warn", scope, args);
|
|
3947
|
+
},
|
|
3948
|
+
error(scope, ...args) {
|
|
3949
|
+
write("error", scope, args);
|
|
3950
|
+
},
|
|
3951
|
+
debug(scope, ...args) {
|
|
3952
|
+
write("debug", scope, args);
|
|
3953
|
+
}
|
|
3954
|
+
};
|
|
3069
3955
|
function _mergeNamespaces$1(e2, t2) {
|
|
3070
3956
|
return t2.forEach((function(t3) {
|
|
3071
3957
|
t3 && "string" != typeof t3 && !Array.isArray(t3) && Object.keys(t3).forEach((function(r2) {
|
|
@@ -5435,6 +6321,7 @@ class NetworkCollector {
|
|
|
5435
6321
|
var _a;
|
|
5436
6322
|
if (!this.running) return;
|
|
5437
6323
|
if (this.isServiceRequest(log.url)) return;
|
|
6324
|
+
if (hasSelfTrafficHeader(log.requestHeaders)) return;
|
|
5438
6325
|
this.logs.push(log);
|
|
5439
6326
|
if (this.logs.length > NETWORK_LOG_CLIENT_BUFFER_SIZE) {
|
|
5440
6327
|
this.logs = this.logs.slice(-NETWORK_LOG_CLIENT_BUFFER_SIZE);
|
|
@@ -5649,64 +6536,6 @@ class NetworkCollector {
|
|
|
5649
6536
|
this.patchedXHR = null;
|
|
5650
6537
|
}
|
|
5651
6538
|
}
|
|
5652
|
-
async function sha256Hex(bytes) {
|
|
5653
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
|
|
5654
|
-
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5655
|
-
}
|
|
5656
|
-
function dropOldestFrames(connections, bytesToDrop) {
|
|
5657
|
-
const order = connections.flatMap(
|
|
5658
|
-
(connection, index) => connection.frames.map((frame) => {
|
|
5659
|
-
var _a;
|
|
5660
|
-
return { index, t: frame.t, bytes: ((_a = frame.data) == null ? void 0 : _a.length) ?? 0 };
|
|
5661
|
-
})
|
|
5662
|
-
).sort((a2, b) => a2.t - b.t);
|
|
5663
|
-
const dropCount = connections.map(() => 0);
|
|
5664
|
-
const dropBytes = connections.map(() => 0);
|
|
5665
|
-
let removed = 0;
|
|
5666
|
-
for (const entry of order) {
|
|
5667
|
-
if (removed >= bytesToDrop) break;
|
|
5668
|
-
dropCount[entry.index] += 1;
|
|
5669
|
-
dropBytes[entry.index] += entry.bytes;
|
|
5670
|
-
removed += entry.bytes;
|
|
5671
|
-
}
|
|
5672
|
-
return connections.map((connection, index) => {
|
|
5673
|
-
const count2 = dropCount[index];
|
|
5674
|
-
if (count2 === 0) return connection;
|
|
5675
|
-
return {
|
|
5676
|
-
...connection,
|
|
5677
|
-
frames: connection.frames.slice(count2),
|
|
5678
|
-
droppedFrames: connection.droppedFrames + count2,
|
|
5679
|
-
droppedBytes: connection.droppedBytes + dropBytes[index]
|
|
5680
|
-
};
|
|
5681
|
-
});
|
|
5682
|
-
}
|
|
5683
|
-
const FIT_ATTEMPTS = 6;
|
|
5684
|
-
async function serializeSocketTranscript(connections, maxBytes = SOCKET_TRANSCRIPT_MAX_BYTES) {
|
|
5685
|
-
const frameCount = connections.reduce(
|
|
5686
|
-
(sum, connection) => sum + connection.frames.length + connection.droppedFrames,
|
|
5687
|
-
0
|
|
5688
|
-
);
|
|
5689
|
-
if (connections.length === 0 || frameCount === 0) return null;
|
|
5690
|
-
let current = connections;
|
|
5691
|
-
let payload = gzip_1(JSON.stringify({ connections: current }));
|
|
5692
|
-
for (let attempt = 0; attempt < FIT_ATTEMPTS && payload.byteLength > maxBytes; attempt += 1) {
|
|
5693
|
-
const storedBytes = current.reduce(
|
|
5694
|
-
(sum, connection) => sum + storedFrameBytes(connection.frames),
|
|
5695
|
-
0
|
|
5696
|
-
);
|
|
5697
|
-
if (storedBytes === 0) break;
|
|
5698
|
-
const overRatio = 1 - maxBytes / payload.byteLength;
|
|
5699
|
-
current = dropOldestFrames(current, storedBytes * Math.min(1, overRatio + 0.1));
|
|
5700
|
-
payload = gzip_1(JSON.stringify({ connections: current }));
|
|
5701
|
-
}
|
|
5702
|
-
return {
|
|
5703
|
-
payload,
|
|
5704
|
-
checksumSha256: await sha256Hex(payload),
|
|
5705
|
-
sizeBytes: payload.byteLength,
|
|
5706
|
-
connectionCount: connections.length,
|
|
5707
|
-
frameCount
|
|
5708
|
-
};
|
|
5709
|
-
}
|
|
5710
6539
|
var NetworkLogType = /* @__PURE__ */ ((NetworkLogType2) => {
|
|
5711
6540
|
NetworkLogType2["MAIN_FRAME"] = "main_frame";
|
|
5712
6541
|
NetworkLogType2["SUB_FRAME"] = "sub_frame";
|
|
@@ -9112,6 +9941,8 @@ var Trigger = PopoverTrigger$1;
|
|
|
9112
9941
|
var Portal = PopoverPortal;
|
|
9113
9942
|
var Content2 = PopoverContent$1;
|
|
9114
9943
|
const FLOATING_LAYER_SELECTOR = "[data-radix-popper-content-wrapper]";
|
|
9944
|
+
const OUTSIDE_EVENTS = ["pointerdown", "focusin"];
|
|
9945
|
+
const roots = /* @__PURE__ */ new Map();
|
|
9115
9946
|
function isInsideLayer(path, content) {
|
|
9116
9947
|
return path.some((node) => {
|
|
9117
9948
|
if (!(node instanceof Element)) return false;
|
|
@@ -9120,7 +9951,48 @@ function isInsideLayer(path, content) {
|
|
|
9120
9951
|
return node.matches(FLOATING_LAYER_SELECTOR) && !node.contains(content);
|
|
9121
9952
|
});
|
|
9122
9953
|
}
|
|
9123
|
-
function
|
|
9954
|
+
function dismissTopLayer(root, event) {
|
|
9955
|
+
var _a;
|
|
9956
|
+
const layers = (_a = roots.get(root)) == null ? void 0 : _a.layers;
|
|
9957
|
+
const top = layers == null ? void 0 : layers[layers.length - 1];
|
|
9958
|
+
if (!top) return;
|
|
9959
|
+
if (event.type === "focusin" && !top.focusOutside) return;
|
|
9960
|
+
if (isInsideLayer(event.composedPath(), top.content)) return;
|
|
9961
|
+
top.dismiss();
|
|
9962
|
+
}
|
|
9963
|
+
function listen(root) {
|
|
9964
|
+
const doc = root.ownerDocument;
|
|
9965
|
+
const onRoot = (event) => dismissTopLayer(root, event);
|
|
9966
|
+
const onDocument = (event) => {
|
|
9967
|
+
if (!event.composedPath().includes(root)) dismissTopLayer(root, event);
|
|
9968
|
+
};
|
|
9969
|
+
OUTSIDE_EVENTS.forEach((type) => {
|
|
9970
|
+
root.addEventListener(type, onRoot);
|
|
9971
|
+
doc.addEventListener(type, onDocument);
|
|
9972
|
+
});
|
|
9973
|
+
return () => OUTSIDE_EVENTS.forEach((type) => {
|
|
9974
|
+
root.removeEventListener(type, onRoot);
|
|
9975
|
+
doc.removeEventListener(type, onDocument);
|
|
9976
|
+
});
|
|
9977
|
+
}
|
|
9978
|
+
function registerLayer(root, layer) {
|
|
9979
|
+
let entry = roots.get(root);
|
|
9980
|
+
if (!entry) {
|
|
9981
|
+
entry = { layers: [], detach: listen(root) };
|
|
9982
|
+
roots.set(root, entry);
|
|
9983
|
+
}
|
|
9984
|
+
entry.layers.push(layer);
|
|
9985
|
+
return () => {
|
|
9986
|
+
const current = roots.get(root);
|
|
9987
|
+
if (!current) return;
|
|
9988
|
+
current.layers = current.layers.filter((item) => item !== layer);
|
|
9989
|
+
if (current.layers.length === 0) {
|
|
9990
|
+
current.detach();
|
|
9991
|
+
roots.delete(root);
|
|
9992
|
+
}
|
|
9993
|
+
};
|
|
9994
|
+
}
|
|
9995
|
+
function useShadowOutsideDismiss(onDismiss, { focusOutside = true } = {}) {
|
|
9124
9996
|
const onDismissRef = useRef(onDismiss);
|
|
9125
9997
|
const detachRef = useRef(null);
|
|
9126
9998
|
useEffect(() => {
|
|
@@ -9130,45 +10002,67 @@ function useShadowOutsideDismiss(onDismiss) {
|
|
|
9130
10002
|
var _a;
|
|
9131
10003
|
return (_a = detachRef.current) == null ? void 0 : _a.call(detachRef);
|
|
9132
10004
|
}, []);
|
|
9133
|
-
return useCallback(
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
10005
|
+
return useCallback(
|
|
10006
|
+
(content) => {
|
|
10007
|
+
var _a;
|
|
10008
|
+
(_a = detachRef.current) == null ? void 0 : _a.call(detachRef);
|
|
10009
|
+
detachRef.current = null;
|
|
10010
|
+
if (!content) return;
|
|
10011
|
+
const root = content.getRootNode();
|
|
10012
|
+
if (!(root instanceof ShadowRoot)) return;
|
|
10013
|
+
detachRef.current = registerLayer(root, {
|
|
10014
|
+
content,
|
|
10015
|
+
focusOutside,
|
|
10016
|
+
dismiss: () => {
|
|
10017
|
+
var _a2;
|
|
10018
|
+
return (_a2 = onDismissRef.current) == null ? void 0 : _a2.call(onDismissRef);
|
|
10019
|
+
}
|
|
10020
|
+
});
|
|
10021
|
+
},
|
|
10022
|
+
[focusOutside]
|
|
10023
|
+
);
|
|
9149
10024
|
}
|
|
9150
|
-
|
|
9151
|
-
void 0
|
|
9152
|
-
)
|
|
9153
|
-
|
|
9154
|
-
|
|
10025
|
+
function createShadowDismissRoot(Root3, options) {
|
|
10026
|
+
const DismissContext = React.createContext(void 0);
|
|
10027
|
+
function ShadowDismissRoot({ open, defaultOpen = false, onOpenChange, ...rest }) {
|
|
10028
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
|
|
10029
|
+
const isControlled = open !== void 0;
|
|
10030
|
+
const setOpen = React.useCallback(
|
|
10031
|
+
(next) => {
|
|
10032
|
+
if (!isControlled) setUncontrolledOpen(next);
|
|
10033
|
+
onOpenChange == null ? void 0 : onOpenChange(next);
|
|
10034
|
+
},
|
|
10035
|
+
[isControlled, onOpenChange]
|
|
10036
|
+
);
|
|
10037
|
+
const dismiss2 = React.useCallback(() => setOpen(false), [setOpen]);
|
|
10038
|
+
return /* @__PURE__ */ jsx(DismissContext.Provider, { value: dismiss2, children: /* @__PURE__ */ jsx(
|
|
10039
|
+
Root3,
|
|
10040
|
+
{
|
|
10041
|
+
...rest,
|
|
10042
|
+
open: isControlled ? open : uncontrolledOpen,
|
|
10043
|
+
onOpenChange: setOpen
|
|
10044
|
+
}
|
|
10045
|
+
) });
|
|
10046
|
+
}
|
|
10047
|
+
function useContentDismissRef(forwardedRef) {
|
|
10048
|
+
const dismissRef = useShadowOutsideDismiss(React.useContext(DismissContext), options);
|
|
10049
|
+
return React.useCallback(
|
|
10050
|
+
(node) => {
|
|
10051
|
+
dismissRef(node);
|
|
10052
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
10053
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
10054
|
+
},
|
|
10055
|
+
[dismissRef, forwardedRef]
|
|
10056
|
+
);
|
|
10057
|
+
}
|
|
10058
|
+
return [ShadowDismissRoot, useContentDismissRef];
|
|
9155
10059
|
}
|
|
10060
|
+
const [Popover, usePopoverContentDismissRef] = createShadowDismissRoot(Root2);
|
|
9156
10061
|
const PopoverTrigger = Trigger;
|
|
9157
10062
|
const PopoverAnchor = Anchor2;
|
|
9158
10063
|
const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 6, onEscapeKeyDown, ...props }, ref) => {
|
|
9159
10064
|
const portalContainer = usePortalContainer();
|
|
9160
|
-
const
|
|
9161
|
-
const dismissRef = useShadowOutsideDismiss(
|
|
9162
|
-
React.useCallback(() => onOpenChange == null ? void 0 : onOpenChange(false), [onOpenChange])
|
|
9163
|
-
);
|
|
9164
|
-
const contentRef = React.useCallback(
|
|
9165
|
-
(node) => {
|
|
9166
|
-
dismissRef(node);
|
|
9167
|
-
if (typeof ref === "function") ref(node);
|
|
9168
|
-
else if (ref) ref.current = node;
|
|
9169
|
-
},
|
|
9170
|
-
[dismissRef, ref]
|
|
9171
|
-
);
|
|
10065
|
+
const contentRef = usePopoverContentDismissRef(ref);
|
|
9172
10066
|
return /* @__PURE__ */ jsx(Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
|
|
9173
10067
|
Content2,
|
|
9174
10068
|
{
|
|
@@ -10554,7 +11448,9 @@ const X = createLucideIcon("X", [
|
|
|
10554
11448
|
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
|
|
10555
11449
|
["path", { d: "m6 6 12 12", key: "d8bk6v" }]
|
|
10556
11450
|
]);
|
|
10557
|
-
const Select$1 = Select$2
|
|
11451
|
+
const [Select$1, useSelectContentDismissRef] = createShadowDismissRoot(Select$2, {
|
|
11452
|
+
focusOutside: false
|
|
11453
|
+
});
|
|
10558
11454
|
const SelectValue = SelectValue$1;
|
|
10559
11455
|
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
10560
11456
|
SelectTrigger$1,
|
|
@@ -10606,10 +11502,11 @@ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) =
|
|
|
10606
11502
|
SelectScrollDownButton.displayName = SelectScrollDownButton$1.displayName;
|
|
10607
11503
|
const SelectContent = React.forwardRef(({ className, children, position = "popper", onEscapeKeyDown, ...props }, ref) => {
|
|
10608
11504
|
const portalContainer = usePortalContainer();
|
|
11505
|
+
const contentRef = useSelectContentDismissRef(ref);
|
|
10609
11506
|
return /* @__PURE__ */ jsx(SelectPortal, { container: portalContainer, children: /* @__PURE__ */ jsxs(
|
|
10610
11507
|
SelectContent$1,
|
|
10611
11508
|
{
|
|
10612
|
-
ref,
|
|
11509
|
+
ref: contentRef,
|
|
10613
11510
|
"data-testid": "select-content",
|
|
10614
11511
|
sideOffset: 6,
|
|
10615
11512
|
onEscapeKeyDown: stopEscapePropagation(onEscapeKeyDown),
|
|
@@ -16311,50 +17208,6 @@ function useOnboardingFlow() {
|
|
|
16311
17208
|
[context]
|
|
16312
17209
|
);
|
|
16313
17210
|
}
|
|
16314
|
-
let nextId = 0;
|
|
16315
|
-
let toasts = [];
|
|
16316
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
16317
|
-
function emit() {
|
|
16318
|
-
listeners.forEach((l2) => l2());
|
|
16319
|
-
}
|
|
16320
|
-
function addToast(message2, type, options) {
|
|
16321
|
-
const id = `toast-${++nextId}`;
|
|
16322
|
-
const t2 = {
|
|
16323
|
-
id,
|
|
16324
|
-
message: message2,
|
|
16325
|
-
type,
|
|
16326
|
-
action: options == null ? void 0 : options.action,
|
|
16327
|
-
duration: (options == null ? void 0 : options.duration) ?? 2e3,
|
|
16328
|
-
createdAt: Date.now()
|
|
16329
|
-
};
|
|
16330
|
-
toasts = [...toasts, t2];
|
|
16331
|
-
emit();
|
|
16332
|
-
return id;
|
|
16333
|
-
}
|
|
16334
|
-
function dismiss(id) {
|
|
16335
|
-
toasts = toasts.filter((t2) => t2.id !== id);
|
|
16336
|
-
emit();
|
|
16337
|
-
}
|
|
16338
|
-
function dismissAll() {
|
|
16339
|
-
toasts = [];
|
|
16340
|
-
emit();
|
|
16341
|
-
}
|
|
16342
|
-
function toast(message2, options) {
|
|
16343
|
-
return addToast(message2, "default", options);
|
|
16344
|
-
}
|
|
16345
|
-
toast.success = (message2, options) => addToast(message2, "success", options);
|
|
16346
|
-
toast.warning = (message2, options) => addToast(message2, "warning", options);
|
|
16347
|
-
toast.error = (message2, options) => addToast(message2, "error", options);
|
|
16348
|
-
toast.info = (message2, options) => addToast(message2, "info", options);
|
|
16349
|
-
toast.dismiss = dismiss;
|
|
16350
|
-
toast.dismissAll = dismissAll;
|
|
16351
|
-
function subscribe(listener) {
|
|
16352
|
-
listeners.add(listener);
|
|
16353
|
-
return () => listeners.delete(listener);
|
|
16354
|
-
}
|
|
16355
|
-
function getSnapshot() {
|
|
16356
|
-
return toasts;
|
|
16357
|
-
}
|
|
16358
17211
|
({
|
|
16359
17212
|
type: [NetworkLogType.XMLHTTPREQUEST, NetworkLogType.FETCH]
|
|
16360
17213
|
});
|
|
@@ -16496,20 +17349,6 @@ function filterExcludeServiceRelatedConsoleLogs(consoleLogs) {
|
|
|
16496
17349
|
return !isIssueStickerInternalConsoleLog(log) && !isIssueStickerServiceConsoleLog(log);
|
|
16497
17350
|
});
|
|
16498
17351
|
}
|
|
16499
|
-
const TRANSCRIPT_CONTENT_TYPE = "application/gzip";
|
|
16500
|
-
async function uploadSocketTranscript(api, snapshot, options = {}) {
|
|
16501
|
-
if (snapshot.sizeBytes > SOCKET_TRANSCRIPT_MAX_BYTES) return null;
|
|
16502
|
-
try {
|
|
16503
|
-
const file = new File([snapshot.payload], "socket-transcript.json.gz", {
|
|
16504
|
-
type: TRANSCRIPT_CONTENT_TYPE
|
|
16505
|
-
});
|
|
16506
|
-
const uploaded = await uploadSignedMediaWithApi(api, file, "socket", options);
|
|
16507
|
-
return { uploadId: uploaded.id };
|
|
16508
|
-
} catch (error) {
|
|
16509
|
-
devLogger.error("socketTranscriptUpload", "소켓 전사 업로드 실패:", error);
|
|
16510
|
-
return null;
|
|
16511
|
-
}
|
|
16512
|
-
}
|
|
16513
17352
|
function buildExternalUserPayload(user) {
|
|
16514
17353
|
if (!user) return {};
|
|
16515
17354
|
return {
|
|
@@ -16518,103 +17357,115 @@ function buildExternalUserPayload(user) {
|
|
|
16518
17357
|
};
|
|
16519
17358
|
}
|
|
16520
17359
|
export {
|
|
16521
|
-
|
|
16522
|
-
|
|
16523
|
-
|
|
17360
|
+
SelectValue as $,
|
|
17361
|
+
SessionReplayCaptureController as A,
|
|
17362
|
+
preloadModernScreenshot as B,
|
|
16524
17363
|
ConsoleCollector as C,
|
|
16525
|
-
|
|
16526
|
-
|
|
16527
|
-
|
|
16528
|
-
|
|
16529
|
-
|
|
17364
|
+
enqueueIssueUploads as D,
|
|
17365
|
+
normalizeUploadMimeType as E,
|
|
17366
|
+
toSessionReplayMeta as F,
|
|
17367
|
+
loadTusClient as G,
|
|
17368
|
+
createIndexedDbPendingUploadStore as H,
|
|
16530
17369
|
ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
|
|
16531
|
-
|
|
16532
|
-
|
|
16533
|
-
|
|
16534
|
-
|
|
17370
|
+
ATTACHMENT_UPLOAD_DB_NAME as J,
|
|
17371
|
+
createMediaUploadApi as K,
|
|
17372
|
+
toast as L,
|
|
17373
|
+
trimLogBackupPayload as M,
|
|
16535
17374
|
NetworkCollector as N,
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
17375
|
+
createSafeLogBackup as O,
|
|
17376
|
+
getLogBackupSessionId as P,
|
|
17377
|
+
createIndexedDbLogBackup as Q,
|
|
17378
|
+
Popover as R,
|
|
16540
17379
|
ScreenRecordingController as S,
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
17380
|
+
PopoverTrigger as T,
|
|
17381
|
+
Input as U,
|
|
17382
|
+
VIDEO_UPLOAD_DB_NAME as V,
|
|
16544
17383
|
WebSocketCollector as W,
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
16548
|
-
|
|
17384
|
+
PopoverContent as X,
|
|
17385
|
+
Label as Y,
|
|
17386
|
+
Select$1 as Z,
|
|
17387
|
+
SelectTrigger as _,
|
|
16549
17388
|
uploadSignedMediaWithApi as a,
|
|
16550
|
-
|
|
16551
|
-
|
|
16552
|
-
|
|
16553
|
-
|
|
16554
|
-
|
|
16555
|
-
|
|
16556
|
-
|
|
16557
|
-
|
|
16558
|
-
|
|
16559
|
-
|
|
16560
|
-
|
|
16561
|
-
|
|
16562
|
-
|
|
16563
|
-
|
|
16564
|
-
|
|
16565
|
-
|
|
16566
|
-
|
|
16567
|
-
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16571
|
-
|
|
16572
|
-
|
|
16573
|
-
|
|
16574
|
-
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
|
|
16578
|
-
|
|
16579
|
-
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16585
|
-
|
|
16586
|
-
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
16592
|
-
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
17389
|
+
SelectContent as a0,
|
|
17390
|
+
SelectItem as a1,
|
|
17391
|
+
JIRA_ISSUE_TYPE_VALUE_KEY as a2,
|
|
17392
|
+
getJiraMetadata as a3,
|
|
17393
|
+
buildJiraIssueFields as a4,
|
|
17394
|
+
getJiraIssueTypeLabel as a5,
|
|
17395
|
+
Button$1 as a6,
|
|
17396
|
+
Root2 as a7,
|
|
17397
|
+
Trigger as a8,
|
|
17398
|
+
ChevronDown as a9,
|
|
17399
|
+
useShadowOutsideDismiss as aA,
|
|
17400
|
+
getPrimaryNotionMapping as aB,
|
|
17401
|
+
buttonVariants as aC,
|
|
17402
|
+
TEST_ATTR_KEYS as aD,
|
|
17403
|
+
jaccard as aE,
|
|
17404
|
+
textSimilarity as aF,
|
|
17405
|
+
collectHeadings as aG,
|
|
17406
|
+
buildElementLocator as aH,
|
|
17407
|
+
normalizeForCompare as aI,
|
|
17408
|
+
CONTAINER_SELECTOR as aJ,
|
|
17409
|
+
safeRole as aK,
|
|
17410
|
+
safeAccessibleName as aL,
|
|
17411
|
+
sameDigitGroups as aM,
|
|
17412
|
+
labelSimilarityKeepingDigits as aN,
|
|
17413
|
+
landmarkRole as aO,
|
|
17414
|
+
findElementBySelector as aP,
|
|
17415
|
+
findElementByXPath as aQ,
|
|
17416
|
+
OnboardingProvider as aR,
|
|
17417
|
+
useOnboardingTarget as aS,
|
|
17418
|
+
useOnboardingFlow as aT,
|
|
17419
|
+
PopoverAnchor as aU,
|
|
17420
|
+
Portal as aa,
|
|
17421
|
+
Content2 as ab,
|
|
17422
|
+
Check as ac,
|
|
17423
|
+
X as ad,
|
|
17424
|
+
DatePicker as ae,
|
|
17425
|
+
getNotionPageMetadata as af,
|
|
17426
|
+
buildNotionFieldViews as ag,
|
|
17427
|
+
DirectUploadError as ah,
|
|
17428
|
+
getRegisteredUploadId as ai,
|
|
17429
|
+
getSnapshot as aj,
|
|
17430
|
+
subscribe as ak,
|
|
17431
|
+
ChevronRight as al,
|
|
17432
|
+
ReactRemoveScroll as am,
|
|
17433
|
+
hideOthers as an,
|
|
17434
|
+
useFocusGuards as ao,
|
|
17435
|
+
FocusScope as ap,
|
|
17436
|
+
createShadowDismissRoot as aq,
|
|
17437
|
+
getExposedJiraFields as ar,
|
|
17438
|
+
isJiraFieldExposed as as,
|
|
17439
|
+
normalizeExposedJiraFields as at,
|
|
17440
|
+
resolveJiraIssueTypeId as au,
|
|
17441
|
+
pickJiraIssueTypeFields as av,
|
|
17442
|
+
switchJiraIssueType as aw,
|
|
17443
|
+
getPrimaryJiraMapping as ax,
|
|
17444
|
+
createCollection as ay,
|
|
17445
|
+
useDirection as az,
|
|
17446
|
+
captureWithModernScreenshot as b,
|
|
17447
|
+
captureViewportWithElementCrop as c,
|
|
16597
17448
|
directUploadToSignedUrl as d,
|
|
16598
|
-
|
|
16599
|
-
|
|
17449
|
+
buildJiraCreateFields as e,
|
|
17450
|
+
buildNotionCreateFields as f,
|
|
16600
17451
|
getElementPosition as g,
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
17452
|
+
buildGuestOwnerQuery as h,
|
|
17453
|
+
composeIssueFilterGroups as i,
|
|
17454
|
+
buildIssueIntegrationsPayload as j,
|
|
17455
|
+
devLogger as k,
|
|
17456
|
+
filterExcludeServiceRelatedLogs as l,
|
|
17457
|
+
filterExcludeServiceRelatedConsoleLogs as m,
|
|
17458
|
+
compactNetworkLogsForTransport as n,
|
|
17459
|
+
CONSOLE_LOG_SUBMIT_LIMIT as o,
|
|
16609
17460
|
pristineFetch as p,
|
|
16610
|
-
|
|
16611
|
-
|
|
17461
|
+
buildExternalUserPayload as q,
|
|
17462
|
+
shrinkNetworkLogsToFit as r,
|
|
16612
17463
|
selectNetworkLogsForSubmit as s,
|
|
16613
|
-
|
|
17464
|
+
measureJsonBytes as t,
|
|
16614
17465
|
uploadScreenshotDataUrls as u,
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
17466
|
+
sanitizeString as v,
|
|
17467
|
+
collectAttachmentUploads as w,
|
|
17468
|
+
hasReplayUpload as x,
|
|
17469
|
+
getCurrentPageUrl as y,
|
|
17470
|
+
createIssueUploadQueues as z
|
|
16620
17471
|
};
|