issue-sticker 0.11.0 → 0.13.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 +89 -0
- package/dist/_commonjsHelpers-DWwsNxpa.js +8 -0
- package/dist/{externalUserPayload-BbOEmKZD.js → externalUserPayload-CdzgCq4t.js} +10700 -6882
- package/dist/index-DA6SoSp7.js +3009 -0
- package/dist/issue-sticker-headless-react.js +68 -12
- package/dist/issue-sticker-headless.js +554 -130
- package/dist/issue-sticker-react.js +2306 -906
- package/dist/{three-DQdLNs7v.js → three-FamoZZKK.js} +560 -33
- package/dist/{tooltip-DSCAsWIp.js → tooltip-BVA6ikuJ.js} +1 -1
- package/headless.d.ts +80 -4
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadToSignedUrl, p as pristineFetch, g as getElementPosition, c as
|
|
2
|
-
import {
|
|
1
|
+
import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadToSignedUrl, p as pristineFetch, g as getElementPosition, c as captureViewportAndElementShots, b as captureScreenshotWithMasks, i as isCapturedScreenshot, M as MaskingCaptureError, e as buildJiraCreateFields, f as buildNotionCreateFields, h as buildGuestOwnerQuery, j as composeIssueFilterGroups, k as buildIssueIntegrationsPayload, l as devLogger, W as WebSocketCollector, C as ConsoleCollector, N as NetworkCollector, s as selectNetworkLogsForSubmit, m as filterExcludeServiceRelatedLogs, n as filterExcludeServiceRelatedConsoleLogs, o as compactNetworkLogsForTransport, q as CONSOLE_LOG_SUBMIT_LIMIT, r as buildExternalUserPayload, t as shrinkNetworkLogsToFit, v as measureJsonBytes, w as sanitizeString, x as runMaskedSubmit, y as applySubmitMasking, z as collectAttachmentUploads, A as maskPositionForSubmit, B as hasReplayUpload, D as appendMaskingNotice, E as screenshotExclusionNotice, F as collectSnapshotSelectors, G as MaskingScreenRulesChangedError, H as screenRulesChangedMessage, I as MaskingPolicyUnavailableError, J as MaskingSelectorUnsupportedError, K as assertScreenRulesCovered, L as isMaskingFailure, O as maskScreenshotsForSubmit, P as getCurrentPageUrl, Q as createIssueUploadQueues, S as ScreenRecordingController, R as SessionReplayCaptureController, T as preloadModernScreenshot, U as enqueueIssueUploads, V as normalizeUploadMimeType, X as ISSUE_STICKER_INTERNAL_LOG_PREFIX, Y as MASKING_POLICY_PREFETCH_TIMEOUT_MS, Z as toSessionReplayMeta, _ as loadCaptureSelectors, $ as loadMaskingPolicyForSubmit, a0 as resolveEffectivePolicy, a1 as createMaskingPolicySource, a2 as loadTusClient, a3 as createIndexedDbPendingUploadStore, a4 as ATTACHMENT_UPLOAD_DB_NAME, a5 as VIDEO_UPLOAD_DB_NAME, a6 as createMediaUploadApi, a7 as createMemoryMaskingPolicyCache } from "./externalUserPayload-CdzgCq4t.js";
|
|
2
|
+
import { M as MASKING_COPY, R as ReplayMaskingError } from "./three-FamoZZKK.js";
|
|
3
|
+
import { r } from "./three-FamoZZKK.js";
|
|
3
4
|
import "react/jsx-runtime";
|
|
4
5
|
import "react";
|
|
5
6
|
const WINDOW_MS = 6e4;
|
|
@@ -30,10 +31,11 @@ class AutoIssueRateLimit {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
class IssueStickerHeadlessError extends Error {
|
|
33
|
-
constructor(code, message, cause) {
|
|
34
|
+
constructor(code, message, cause, status) {
|
|
34
35
|
super(message);
|
|
35
36
|
this.code = code;
|
|
36
37
|
this.cause = cause;
|
|
38
|
+
this.status = status;
|
|
37
39
|
this.name = "IssueStickerHeadlessError";
|
|
38
40
|
}
|
|
39
41
|
}
|
|
@@ -98,7 +100,8 @@ class HeadlessTransport {
|
|
|
98
100
|
const execute = (nextHeaders) => pristineFetch(`${getApiBaseUrl()}${endpoint}`, {
|
|
99
101
|
method: options.method ?? "GET",
|
|
100
102
|
headers: nextHeaders,
|
|
101
|
-
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
103
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
104
|
+
...options.cache ? { cache: options.cache } : {}
|
|
102
105
|
});
|
|
103
106
|
let response = await execute(headers);
|
|
104
107
|
if (!options.skipAuth && response.status === 401 && this.options.getAuthMode() === "member") {
|
|
@@ -114,10 +117,12 @@ class HeadlessTransport {
|
|
|
114
117
|
}
|
|
115
118
|
const json = await response.json().catch(() => null);
|
|
116
119
|
if (!response.ok || !(json == null ? void 0 : json.success)) {
|
|
120
|
+
const unauthorizedCode = this.options.getAuthMode() === "member" ? "MEMBER_AUTH_REQUIRED" : "AUTH_FAILED";
|
|
117
121
|
throw new IssueStickerHeadlessError(
|
|
118
|
-
response.status === 401 ?
|
|
122
|
+
response.status === 401 ? unauthorizedCode : "NETWORK_ERROR",
|
|
119
123
|
extractErrorMessage(json == null ? void 0 : json.error, (json == null ? void 0 : json.message) || `요청 실패: ${response.status}`),
|
|
120
|
-
json
|
|
124
|
+
json,
|
|
125
|
+
response.status
|
|
121
126
|
);
|
|
122
127
|
}
|
|
123
128
|
return json;
|
|
@@ -471,28 +476,34 @@ function buildHeadlessCaptureContext({
|
|
|
471
476
|
sessionReplayPreview: null
|
|
472
477
|
};
|
|
473
478
|
}
|
|
479
|
+
const NO_HEADLESS_SCREENSHOTS = {
|
|
480
|
+
screenshots: [],
|
|
481
|
+
maskingDropped: false
|
|
482
|
+
};
|
|
474
483
|
async function captureHeadlessScreenshots({
|
|
475
484
|
target,
|
|
476
|
-
excludeSelectors
|
|
485
|
+
excludeSelectors,
|
|
486
|
+
maskSelectors
|
|
477
487
|
}) {
|
|
478
488
|
const targetIsDocumentRoot = target === document.body || target === document.documentElement;
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return
|
|
494
|
-
|
|
495
|
-
|
|
489
|
+
const viewport = await captureViewportAndElementShots({
|
|
490
|
+
element: target,
|
|
491
|
+
excludeSelectors,
|
|
492
|
+
maskSelectors
|
|
493
|
+
});
|
|
494
|
+
let maskingDropped = Boolean(viewport.maskingDropped);
|
|
495
|
+
const elementShot = targetIsDocumentRoot || viewport.element ? viewport.element : await captureScreenshotWithMasks({
|
|
496
|
+
element: target,
|
|
497
|
+
excludeSelectors,
|
|
498
|
+
maskSelectors
|
|
499
|
+
}).catch((error) => {
|
|
500
|
+
maskingDropped || (maskingDropped = error instanceof MaskingCaptureError);
|
|
501
|
+
return void 0;
|
|
502
|
+
});
|
|
503
|
+
return {
|
|
504
|
+
screenshots: [viewport.full, elementShot].filter(isCapturedScreenshot),
|
|
505
|
+
maskingDropped
|
|
506
|
+
};
|
|
496
507
|
}
|
|
497
508
|
function normalizeCustomInputType(fieldType) {
|
|
498
509
|
if (fieldType === "number") return "number";
|
|
@@ -954,20 +965,6 @@ class HeadlessPendingMedia {
|
|
|
954
965
|
this.file = null;
|
|
955
966
|
}
|
|
956
967
|
}
|
|
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
968
|
function createInitialHeadlessSnapshot() {
|
|
972
969
|
return {
|
|
973
970
|
phase: "idle",
|
|
@@ -993,6 +990,8 @@ function createInitialHeadlessSnapshot() {
|
|
|
993
990
|
enabled: true,
|
|
994
991
|
error: null
|
|
995
992
|
},
|
|
993
|
+
videoUploads: [],
|
|
994
|
+
attachmentUploads: [],
|
|
996
995
|
formSchema: null,
|
|
997
996
|
filterSchema: null,
|
|
998
997
|
error: null
|
|
@@ -1104,7 +1103,8 @@ function buildIssueCreatePayload({
|
|
|
1104
1103
|
uploads,
|
|
1105
1104
|
logs,
|
|
1106
1105
|
currentUrl,
|
|
1107
|
-
userAgent
|
|
1106
|
+
userAgent,
|
|
1107
|
+
maskingPolicyVersion
|
|
1108
1108
|
}) {
|
|
1109
1109
|
var _a, _b, _c;
|
|
1110
1110
|
const title = (_a = draft.title) == null ? void 0 : _a.trim();
|
|
@@ -1117,17 +1117,14 @@ function buildIssueCreatePayload({
|
|
|
1117
1117
|
...intent ? { intent } : {},
|
|
1118
1118
|
position: draft.position,
|
|
1119
1119
|
...uploads.screenshotUploadIds.length > 0 ? { screenshot_upload_ids: uploads.screenshotUploadIds } : {},
|
|
1120
|
-
...uploads.
|
|
1121
|
-
...uploads.
|
|
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 } : {},
|
|
1120
|
+
...uploads.videoPending ? { video_pending: true } : {},
|
|
1121
|
+
...uploads.replayPending ? { session_replay_pending: true } : {},
|
|
1126
1122
|
console_logs: logs.consoleLogs.length > 0 ? logs.consoleLogs : void 0,
|
|
1127
1123
|
user_agent: userAgent,
|
|
1128
1124
|
tenant_id: auth.tenantId,
|
|
1129
1125
|
project_id: projectId,
|
|
1130
1126
|
account_id: auth.accountId ?? void 0,
|
|
1127
|
+
masking_policy_version: maskingPolicyVersion,
|
|
1131
1128
|
...buildExternalUserPayload(config.user)
|
|
1132
1129
|
};
|
|
1133
1130
|
const { networkLogs, stillOverBudget } = shrinkNetworkLogsToFit(
|
|
@@ -1142,8 +1139,67 @@ function buildIssueCreatePayload({
|
|
|
1142
1139
|
stillOverBudget
|
|
1143
1140
|
};
|
|
1144
1141
|
}
|
|
1142
|
+
async function prepareHeadlessScreenAttachments(input) {
|
|
1143
|
+
var _a, _b;
|
|
1144
|
+
const { context } = input;
|
|
1145
|
+
const includeScreenshots = input.draft.mediaMode === "screenshot" || Boolean((_a = context.rules) == null ? void 0 : _a.screen.exclude_video);
|
|
1146
|
+
const screenshots = includeScreenshots ? input.screenshots : [];
|
|
1147
|
+
const sessionReplay = input.draft.sessionReplayEnabled ? input.sessionReplay : null;
|
|
1148
|
+
await (sessionReplay == null ? void 0 : sessionReplay.whenRestarted());
|
|
1149
|
+
const memoSelectors = (sessionReplay == null ? void 0 : sessionReplay.getMaskMemoSelectors()) ?? null;
|
|
1150
|
+
assertScreenRulesCovered(context.rules, [
|
|
1151
|
+
...screenshots.map((shot) => shot.masks.selectors),
|
|
1152
|
+
...memoSelectors ? [memoSelectors] : []
|
|
1153
|
+
]);
|
|
1154
|
+
const replay = sessionReplay ? await sessionReplay.takeSnapshot(((_b = context.rules) == null ? void 0 : _b.screen.mask_selectors) ?? []).catch((error) => {
|
|
1155
|
+
if (isMaskingFailure(error)) throw error;
|
|
1156
|
+
return null;
|
|
1157
|
+
}) : null;
|
|
1158
|
+
const uploads = includeScreenshots ? await input.transport.uploadScreenshotPreviews(
|
|
1159
|
+
await maskScreenshotsForSubmit(screenshots, context.rules)
|
|
1160
|
+
) : [];
|
|
1161
|
+
return {
|
|
1162
|
+
screenshotUploadIds: uploads.map((upload) => upload.id),
|
|
1163
|
+
replay,
|
|
1164
|
+
screenshotExcluded: includeScreenshots && input.screenshotsDroppedByMasking ? uploads.length > 0 ? "partial" : "all" : null
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function toHeadlessMaskingError(error, options) {
|
|
1168
|
+
var _a;
|
|
1169
|
+
if (error instanceof MaskingScreenRulesChangedError) {
|
|
1170
|
+
(_a = options.onScreenRulesChanged) == null ? void 0 : _a.call(options);
|
|
1171
|
+
return new IssueStickerHeadlessError(
|
|
1172
|
+
"MASKING_POLICY_CHANGED",
|
|
1173
|
+
screenRulesChangedMessage({
|
|
1174
|
+
screenshots: options.recapturesScreenshots,
|
|
1175
|
+
replay: options.replayRecording
|
|
1176
|
+
}),
|
|
1177
|
+
error
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
if (error instanceof MaskingPolicyUnavailableError) {
|
|
1181
|
+
return new IssueStickerHeadlessError(
|
|
1182
|
+
"MASKING_POLICY_UNAVAILABLE",
|
|
1183
|
+
MASKING_COPY.policyUnavailable,
|
|
1184
|
+
error
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
if (error instanceof MaskingSelectorUnsupportedError) {
|
|
1188
|
+
return new IssueStickerHeadlessError(
|
|
1189
|
+
"MASKING_SELECTOR_UNSUPPORTED",
|
|
1190
|
+
MASKING_COPY.selectorUnsupported,
|
|
1191
|
+
error
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (error instanceof ReplayMaskingError) {
|
|
1195
|
+
options.restartReplay();
|
|
1196
|
+
return new IssueStickerHeadlessError("CAPTURE_FAILED", MASKING_COPY.replayMaskFailed, error);
|
|
1197
|
+
}
|
|
1198
|
+
return error;
|
|
1199
|
+
}
|
|
1145
1200
|
async function submitHeadlessIssueDraft({
|
|
1146
1201
|
draft,
|
|
1202
|
+
target,
|
|
1147
1203
|
pendingVideoFile,
|
|
1148
1204
|
transport,
|
|
1149
1205
|
issueRepository,
|
|
@@ -1154,41 +1210,104 @@ async function submitHeadlessIssueDraft({
|
|
|
1154
1210
|
logs,
|
|
1155
1211
|
currentUrl,
|
|
1156
1212
|
userAgent,
|
|
1157
|
-
|
|
1158
|
-
|
|
1213
|
+
screenshots,
|
|
1214
|
+
screenshotsDroppedByMasking,
|
|
1215
|
+
sessionReplay,
|
|
1216
|
+
getSocketConnections,
|
|
1217
|
+
maskingPolicy,
|
|
1218
|
+
onScreenRulesChanged,
|
|
1219
|
+
recapturesScreenshots
|
|
1159
1220
|
}) {
|
|
1160
|
-
const
|
|
1161
|
-
const
|
|
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;
|
|
1172
|
-
const { payload: issueData, stillOverBudget } = buildIssueCreatePayload({
|
|
1173
|
-
draft,
|
|
1174
|
-
auth,
|
|
1175
|
-
config,
|
|
1176
|
-
projectId,
|
|
1177
|
-
uploads: { screenshotUploadIds, videoUpload, replayUpload, socketUpload },
|
|
1178
|
-
logs,
|
|
1179
|
-
currentUrl,
|
|
1180
|
-
userAgent
|
|
1181
|
-
});
|
|
1182
|
-
if (stillOverBudget) {
|
|
1183
|
-
throw new IssueStickerHeadlessError(
|
|
1184
|
-
"PAYLOAD_TOO_LARGE",
|
|
1185
|
-
"이슈 데이터가 전송 한도를 넘어 저장할 수 없습니다. 설명을 줄이거나 첨부를 빼고 다시 시도해주세요."
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1221
|
+
const hasVideo = draft.mediaMode === "video" && Boolean(pendingVideoFile);
|
|
1222
|
+
const socketConnections = getSocketConnections();
|
|
1188
1223
|
const integrations = await issueRepository.buildIntegrationsPayload(draft, formSchema);
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1224
|
+
let result;
|
|
1225
|
+
try {
|
|
1226
|
+
result = await runMaskedSubmit({
|
|
1227
|
+
source: maskingPolicy,
|
|
1228
|
+
projectId,
|
|
1229
|
+
prepareScreen: (context) => prepareHeadlessScreenAttachments({
|
|
1230
|
+
context,
|
|
1231
|
+
draft,
|
|
1232
|
+
transport,
|
|
1233
|
+
screenshots,
|
|
1234
|
+
screenshotsDroppedByMasking,
|
|
1235
|
+
sessionReplay
|
|
1236
|
+
}),
|
|
1237
|
+
submit: async (context, screen) => {
|
|
1238
|
+
var _a, _b, _c;
|
|
1239
|
+
const masked = applySubmitMasking({
|
|
1240
|
+
snapshot: context.snapshot,
|
|
1241
|
+
projectId,
|
|
1242
|
+
networkLogs: logs.networkLogs,
|
|
1243
|
+
consoleLogs: logs.consoleLogs,
|
|
1244
|
+
socketConnections,
|
|
1245
|
+
pageUrl: currentUrl
|
|
1246
|
+
});
|
|
1247
|
+
const videoExcluded = hasVideo && Boolean((_a = masked.rules) == null ? void 0 : _a.screen.exclude_video);
|
|
1248
|
+
const replay = screen.replay;
|
|
1249
|
+
const attachments = await collectAttachmentUploads({
|
|
1250
|
+
takeReplay: replay ? async () => replay : void 0,
|
|
1251
|
+
getConnections: () => masked.socketConnections
|
|
1252
|
+
});
|
|
1253
|
+
const maskSelectors = ((_b = context.rules) == null ? void 0 : _b.screen.mask_selectors) ?? [];
|
|
1254
|
+
const position = {
|
|
1255
|
+
...draft.position,
|
|
1256
|
+
...maskPositionForSubmit(draft.position, target, maskSelectors)
|
|
1257
|
+
};
|
|
1258
|
+
const { payload: issueData, stillOverBudget } = buildIssueCreatePayload({
|
|
1259
|
+
draft: {
|
|
1260
|
+
...draft,
|
|
1261
|
+
position,
|
|
1262
|
+
// 마스킹 정책 때문에 버린 스크린샷은 이슈에도 남긴다. 말없이 빼지 않는다.
|
|
1263
|
+
...screen.screenshotExcluded ? {
|
|
1264
|
+
description: appendMaskingNotice(
|
|
1265
|
+
draft.description,
|
|
1266
|
+
screenshotExclusionNotice(screen.screenshotExcluded)
|
|
1267
|
+
)
|
|
1268
|
+
} : {}
|
|
1269
|
+
},
|
|
1270
|
+
auth,
|
|
1271
|
+
config,
|
|
1272
|
+
projectId,
|
|
1273
|
+
uploads: {
|
|
1274
|
+
screenshotUploadIds: screen.screenshotUploadIds,
|
|
1275
|
+
videoPending: hasVideo && !videoExcluded,
|
|
1276
|
+
replayPending: hasReplayUpload(attachments)
|
|
1277
|
+
},
|
|
1278
|
+
logs: { networkLogs: masked.networkLogs, consoleLogs: masked.consoleLogs },
|
|
1279
|
+
currentUrl: masked.pageUrl,
|
|
1280
|
+
userAgent,
|
|
1281
|
+
maskingPolicyVersion: masked.version
|
|
1282
|
+
});
|
|
1283
|
+
if (stillOverBudget) {
|
|
1284
|
+
throw new IssueStickerHeadlessError(
|
|
1285
|
+
"PAYLOAD_TOO_LARGE",
|
|
1286
|
+
"이슈 데이터가 전송 한도를 넘어 저장할 수 없습니다. 설명을 줄이거나 첨부를 빼고 다시 시도해주세요."
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
const issue = await issueRepository.createIssue({ ...issueData, integrations });
|
|
1290
|
+
if (draft.sessionReplayEnabled && ((_c = sessionReplay.getMaskMemoSelectors()) == null ? void 0 : _c.length) === 0 && collectSnapshotSelectors(context.snapshot).length > 0) {
|
|
1291
|
+
sessionReplay.restartRecording();
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
issue,
|
|
1295
|
+
attachments,
|
|
1296
|
+
videoExcluded,
|
|
1297
|
+
screenshotExcluded: screen.screenshotExcluded !== null
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
throw toHeadlessMaskingError(error, {
|
|
1303
|
+
onScreenRulesChanged,
|
|
1304
|
+
replayRecording: sessionReplay.isRecording(),
|
|
1305
|
+
recapturesScreenshots,
|
|
1306
|
+
restartReplay: () => sessionReplay.restartRecording()
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
await issueRepository.syncDynamicFields(result.issue, draft, formSchema);
|
|
1310
|
+
return result;
|
|
1192
1311
|
}
|
|
1193
1312
|
const HEADLESS_CAPTURE_STYLE_ID = "issue-sticker-headless-capture-style";
|
|
1194
1313
|
const HEADLESS_CAPTURE_STYLE = `
|
|
@@ -1280,6 +1399,28 @@ const DEFAULT_EXCLUDE_SELECTORS = [
|
|
|
1280
1399
|
"#issue-sticker-host",
|
|
1281
1400
|
'[data-issue-sticker-headless-auth="true"]'
|
|
1282
1401
|
];
|
|
1402
|
+
function toHeadlessVideoUpload(state) {
|
|
1403
|
+
return {
|
|
1404
|
+
issueId: state.issueId,
|
|
1405
|
+
status: state.status,
|
|
1406
|
+
progress: state.bytesTotal > 0 ? state.bytesSent / state.bytesTotal : 0
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function toHeadlessAttachmentUpload(state) {
|
|
1410
|
+
return {
|
|
1411
|
+
issueId: state.issueId,
|
|
1412
|
+
kind: state.mediaType === "socket" ? "socket" : "replay",
|
|
1413
|
+
status: state.status,
|
|
1414
|
+
progress: state.bytesTotal > 0 ? state.bytesSent / state.bytesTotal : 0
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
function withExclusions(issue, flags) {
|
|
1418
|
+
return {
|
|
1419
|
+
...issue,
|
|
1420
|
+
...flags.videoExcluded ? { videoExcluded: true } : {},
|
|
1421
|
+
...flags.screenshotExcluded ? { screenshotExcluded: true } : {}
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1283
1424
|
class HeadlessClientImpl {
|
|
1284
1425
|
constructor(config) {
|
|
1285
1426
|
var _a;
|
|
@@ -1290,7 +1431,14 @@ class HeadlessClientImpl {
|
|
|
1290
1431
|
this.initializePromise = null;
|
|
1291
1432
|
this.captureCleanup = null;
|
|
1292
1433
|
this.captureSession = null;
|
|
1434
|
+
this.livePolicySource = {
|
|
1435
|
+
getMaskingPolicySnapshot: () => this.maskingPolicy.getMaskingPolicySnapshot(),
|
|
1436
|
+
refreshMaskingPolicySnapshot: (options) => this.maskingPolicy.refreshMaskingPolicySnapshot(options)
|
|
1437
|
+
};
|
|
1438
|
+
this.maskingPolicyPrefetch = null;
|
|
1439
|
+
this.maskingPolicyFetchedAt = null;
|
|
1293
1440
|
this.suppressNextVideoStopSelection = false;
|
|
1441
|
+
this.destroyed = false;
|
|
1294
1442
|
if (!((_a = config.apiKey) == null ? void 0 : _a.trim())) {
|
|
1295
1443
|
throw new IssueStickerHeadlessError("VALIDATION_ERROR", "apiKey가 필요합니다.");
|
|
1296
1444
|
}
|
|
@@ -1298,12 +1446,13 @@ class HeadlessClientImpl {
|
|
|
1298
1446
|
apiKey: config.apiKey,
|
|
1299
1447
|
getTenantId: () => this.snapshot.auth.tenantId,
|
|
1300
1448
|
getProjectId: () => this.snapshot.auth.projectId,
|
|
1301
|
-
onToken: (result) => this.
|
|
1449
|
+
onToken: (result) => this.applyBridgeToken(result)
|
|
1302
1450
|
});
|
|
1303
1451
|
this.transport = new HeadlessTransport({
|
|
1304
1452
|
getAuthMode: () => this.snapshot.auth.mode,
|
|
1305
1453
|
resolveAuthorizationHeader: (forceRefreshMemberToken) => this.resolveAuthorizationHeader(forceRefreshMemberToken)
|
|
1306
1454
|
});
|
|
1455
|
+
this.maskingPolicy = this.createMaskingPolicy();
|
|
1307
1456
|
this.issueRepository = new HeadlessIssueRepository({
|
|
1308
1457
|
transport: this.transport,
|
|
1309
1458
|
getProjectId: () => this.state.requireProjectId(),
|
|
@@ -1314,6 +1463,22 @@ class HeadlessClientImpl {
|
|
|
1314
1463
|
},
|
|
1315
1464
|
getCurrentUrl: () => getCurrentPageUrl() || void 0
|
|
1316
1465
|
});
|
|
1466
|
+
this.uploadQueues = createIssueUploadQueues({
|
|
1467
|
+
api: createMediaUploadApi({
|
|
1468
|
+
post: (endpoint, body) => this.transport.request(endpoint, { method: "POST", body })
|
|
1469
|
+
}),
|
|
1470
|
+
clientInfo: "issue-sticker-sdk",
|
|
1471
|
+
videoStore: createIndexedDbPendingUploadStore(VIDEO_UPLOAD_DB_NAME),
|
|
1472
|
+
attachmentStore: createIndexedDbPendingUploadStore(ATTACHMENT_UPLOAD_DB_NAME),
|
|
1473
|
+
loadTus: loadTusClient,
|
|
1474
|
+
onEvent: (event) => event.mediaType === "video" ? void this.handleVideoUploadEvent(event) : this.handleAttachmentUploadEvent(event)
|
|
1475
|
+
});
|
|
1476
|
+
this.uploadQueues.video.subscribe((states) => {
|
|
1477
|
+
this.state.setSnapshot({ videoUploads: states.map(toHeadlessVideoUpload) });
|
|
1478
|
+
});
|
|
1479
|
+
this.uploadQueues.attachments.subscribe((states) => {
|
|
1480
|
+
this.state.setSnapshot({ attachmentUploads: states.map(toHeadlessAttachmentUpload) });
|
|
1481
|
+
});
|
|
1317
1482
|
this.logCollectors = new HeadlessLogCollectors(this.config, getApiBaseUrl);
|
|
1318
1483
|
this.screenRecordingController = new ScreenRecordingController({
|
|
1319
1484
|
onStopped: (file) => {
|
|
@@ -1330,7 +1495,14 @@ class HeadlessClientImpl {
|
|
|
1330
1495
|
});
|
|
1331
1496
|
});
|
|
1332
1497
|
this.sessionReplayController = new SessionReplayCaptureController({
|
|
1333
|
-
loadCapabilities: () => this.request("/api/session-replays/capabilities")
|
|
1498
|
+
loadCapabilities: () => this.request("/api/session-replays/capabilities"),
|
|
1499
|
+
// 녹화는 초기화(initializeInternal)가 정책 요청을 띄운 뒤에 시작한다. 같은 요청을 기다린다.
|
|
1500
|
+
maskingPolicy: {
|
|
1501
|
+
source: this.livePolicySource,
|
|
1502
|
+
pendingRefresh: () => this.maskingPolicyPrefetch,
|
|
1503
|
+
refreshedWithin: (maxAgeMs) => this.maskingPolicyFetchedAt !== null && Date.now() - this.maskingPolicyFetchedAt <= maxAgeMs
|
|
1504
|
+
},
|
|
1505
|
+
reportInProgress: () => this.snapshot.draft !== null
|
|
1334
1506
|
});
|
|
1335
1507
|
this.sessionReplayController.subscribe((sessionReplay) => {
|
|
1336
1508
|
this.applySessionReplaySnapshot(sessionReplay);
|
|
@@ -1366,7 +1538,7 @@ class HeadlessClientImpl {
|
|
|
1366
1538
|
this.state.setSnapshot({ phase: "loadingIssues", error: null });
|
|
1367
1539
|
try {
|
|
1368
1540
|
const issues = await this.issueRepository.fetchAllIssueStickers(filters);
|
|
1369
|
-
this.state.setSnapshot({ issues, phase: "idle" });
|
|
1541
|
+
this.state.setSnapshot({ issues: this.keepKnownVideos(issues), phase: "idle" });
|
|
1370
1542
|
} catch (error) {
|
|
1371
1543
|
const nextError = this.toHeadlessError(error);
|
|
1372
1544
|
this.state.setSnapshot({ phase: "idle", error: nextError });
|
|
@@ -1502,18 +1674,24 @@ class HeadlessClientImpl {
|
|
|
1502
1674
|
this.state.setSnapshot({ phase: "submittingIssue", error: null });
|
|
1503
1675
|
try {
|
|
1504
1676
|
const session = this.captureSession;
|
|
1505
|
-
const
|
|
1677
|
+
const captured = (session == null ? void 0 : session.screenshots) ? await session.screenshots : NO_HEADLESS_SCREENSHOTS;
|
|
1506
1678
|
const formSchema = this.snapshot.formSchema ?? ((session == null ? void 0 : session.formSchema) ? await session.formSchema : null);
|
|
1507
1679
|
const submission = {
|
|
1508
1680
|
...this.state.requireDraft(),
|
|
1509
|
-
screenshotPreviewUrls,
|
|
1510
1681
|
title: input.title,
|
|
1511
1682
|
description: input.description,
|
|
1512
1683
|
intent: input.intent,
|
|
1513
1684
|
dynamicFields: input.dynamicFields ?? {}
|
|
1514
1685
|
};
|
|
1515
|
-
const
|
|
1686
|
+
const videoFile = submission.mediaMode === "video" ? this.pendingMedia.videoFile : null;
|
|
1687
|
+
const {
|
|
1688
|
+
issue: createdIssue,
|
|
1689
|
+
attachments,
|
|
1690
|
+
videoExcluded,
|
|
1691
|
+
screenshotExcluded
|
|
1692
|
+
} = await submitHeadlessIssueDraft({
|
|
1516
1693
|
draft: submission,
|
|
1694
|
+
target: (session == null ? void 0 : session.target) ?? null,
|
|
1517
1695
|
pendingVideoFile: this.pendingMedia.videoFile,
|
|
1518
1696
|
transport: this.transport,
|
|
1519
1697
|
issueRepository: this.issueRepository,
|
|
@@ -1524,8 +1702,27 @@ class HeadlessClientImpl {
|
|
|
1524
1702
|
logs: this.logCollectors.collect(),
|
|
1525
1703
|
currentUrl: getCurrentPageUrl(),
|
|
1526
1704
|
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
1527
|
-
|
|
1528
|
-
|
|
1705
|
+
screenshots: captured.screenshots,
|
|
1706
|
+
screenshotsDroppedByMasking: captured.maskingDropped,
|
|
1707
|
+
sessionReplay: this.submitReplay(),
|
|
1708
|
+
getSocketConnections: () => this.logCollectors.getSocketConnections(),
|
|
1709
|
+
maskingPolicy: this.livePolicySource,
|
|
1710
|
+
// 화면 규칙이 바뀌어 제출이 실패하면 불린다. 같은 대상을 다시 찍어 미리보기를 바꾸고
|
|
1711
|
+
// 리플레이 녹화를 새로 시작한다. 재시작은 거절하지 않는다.
|
|
1712
|
+
onScreenRulesChanged: () => {
|
|
1713
|
+
this.recaptureScreenshots();
|
|
1714
|
+
void this.sessionReplayController.restartRecording();
|
|
1715
|
+
},
|
|
1716
|
+
// 스크린샷 모드로 시작한 캡처만 다시 찍는다(recaptureScreenshots).
|
|
1717
|
+
recapturesScreenshots: Boolean(session == null ? void 0 : session.screenshots)
|
|
1718
|
+
});
|
|
1719
|
+
await enqueueIssueUploads({
|
|
1720
|
+
videoQueue: this.uploadQueues.video,
|
|
1721
|
+
attachmentQueue: this.uploadQueues.attachments,
|
|
1722
|
+
issueId: createdIssue.id,
|
|
1723
|
+
projectKey: this.state.requireProjectId(),
|
|
1724
|
+
video: videoFile && !videoExcluded ? { blob: videoFile, mimeType: normalizeUploadMimeType(videoFile.type, "video") } : null,
|
|
1725
|
+
attachments
|
|
1529
1726
|
});
|
|
1530
1727
|
this.clearCaptureListeners();
|
|
1531
1728
|
this.clearPendingMedia();
|
|
@@ -1536,26 +1733,44 @@ class HeadlessClientImpl {
|
|
|
1536
1733
|
formSchema: null,
|
|
1537
1734
|
issues: [createdIssue, ...this.snapshot.issues]
|
|
1538
1735
|
});
|
|
1539
|
-
return createdIssue;
|
|
1736
|
+
return withExclusions(createdIssue, { videoExcluded, screenshotExcluded });
|
|
1540
1737
|
} catch (error) {
|
|
1541
1738
|
const nextError = this.toHeadlessError(error);
|
|
1542
1739
|
this.state.setSnapshot({ phase: "draftingIssue", error: nextError });
|
|
1543
1740
|
throw nextError;
|
|
1544
1741
|
}
|
|
1545
1742
|
}
|
|
1743
|
+
retryVideoUpload(issueId) {
|
|
1744
|
+
this.uploadQueues.retry(issueId);
|
|
1745
|
+
}
|
|
1546
1746
|
async createIssue(input) {
|
|
1547
1747
|
var _a, _b;
|
|
1748
|
+
const onError = input == null ? void 0 : input.onError;
|
|
1749
|
+
let created = null;
|
|
1548
1750
|
try {
|
|
1549
|
-
if (!((_a = input.title) == null ? void 0 : _a.trim()) || !((_b = input.description) == null ? void 0 : _b.trim())) {
|
|
1550
|
-
return this.skipCreateIssue(
|
|
1751
|
+
if (!((_a = input == null ? void 0 : input.title) == null ? void 0 : _a.trim()) || !((_b = input.description) == null ? void 0 : _b.trim())) {
|
|
1752
|
+
return this.skipCreateIssue(
|
|
1753
|
+
new IssueStickerHeadlessError("VALIDATION_ERROR", "제목과 설명이 모두 필요합니다."),
|
|
1754
|
+
onError
|
|
1755
|
+
);
|
|
1551
1756
|
}
|
|
1552
1757
|
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
1553
|
-
return this.skipCreateIssue(
|
|
1758
|
+
return this.skipCreateIssue(
|
|
1759
|
+
new IssueStickerHeadlessError(
|
|
1760
|
+
"VALIDATION_ERROR",
|
|
1761
|
+
"브라우저 DOM 에서만 이슈를 만들 수 있습니다."
|
|
1762
|
+
),
|
|
1763
|
+
onError
|
|
1764
|
+
);
|
|
1554
1765
|
}
|
|
1555
1766
|
await this.ensureInitialized();
|
|
1556
1767
|
if (!this.autoIssueRateLimit.claim()) {
|
|
1557
1768
|
return this.skipCreateIssue(
|
|
1558
|
-
|
|
1769
|
+
new IssueStickerHeadlessError(
|
|
1770
|
+
"RATE_LIMITED",
|
|
1771
|
+
`${this.autoIssueRateLimit.label}까지 호출할 수 있습니다. 호출 조건을 다시 확인해주세요.`
|
|
1772
|
+
),
|
|
1773
|
+
onError
|
|
1559
1774
|
);
|
|
1560
1775
|
}
|
|
1561
1776
|
const target = input.target ?? document.body;
|
|
@@ -1565,18 +1780,21 @@ class HeadlessClientImpl {
|
|
|
1565
1780
|
sessionReplayEnabled: Boolean(sessionReplay.enabled && sessionReplay.canAttach)
|
|
1566
1781
|
});
|
|
1567
1782
|
const logs = this.logCollectors.collect();
|
|
1568
|
-
const
|
|
1569
|
-
() => captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() })
|
|
1570
|
-
);
|
|
1783
|
+
const captured = input.media === "none" ? NO_HEADLESS_SCREENSHOTS : await this.captureScreenshots(target, await this.freshCaptureSelectors());
|
|
1571
1784
|
const formSchema = this.snapshot.auth.mode === "member" && Object.keys(input.dynamicFields ?? {}).length > 0 ? await this.loadFormSchema({}) : null;
|
|
1572
|
-
const
|
|
1785
|
+
const {
|
|
1786
|
+
issue: createdIssue,
|
|
1787
|
+
attachments,
|
|
1788
|
+
screenshotExcluded
|
|
1789
|
+
} = await submitHeadlessIssueDraft({
|
|
1573
1790
|
draft: {
|
|
1574
1791
|
...draft,
|
|
1575
|
-
screenshotPreviewUrls,
|
|
1792
|
+
screenshotPreviewUrls: [],
|
|
1576
1793
|
title: input.title,
|
|
1577
1794
|
description: input.description,
|
|
1578
1795
|
dynamicFields: input.dynamicFields ?? {}
|
|
1579
1796
|
},
|
|
1797
|
+
target,
|
|
1580
1798
|
pendingVideoFile: null,
|
|
1581
1799
|
transport: this.transport,
|
|
1582
1800
|
issueRepository: this.issueRepository,
|
|
@@ -1587,28 +1805,72 @@ class HeadlessClientImpl {
|
|
|
1587
1805
|
logs,
|
|
1588
1806
|
currentUrl: getCurrentPageUrl(),
|
|
1589
1807
|
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
1590
|
-
|
|
1591
|
-
|
|
1808
|
+
screenshots: captured.screenshots,
|
|
1809
|
+
screenshotsDroppedByMasking: captured.maskingDropped,
|
|
1810
|
+
sessionReplay: this.submitReplay(),
|
|
1811
|
+
getSocketConnections: () => this.logCollectors.getSocketConnections(),
|
|
1812
|
+
maskingPolicy: this.livePolicySource,
|
|
1813
|
+
// 미리보기가 없어 다시 찍지 않는다. 다음 호출이 새 규칙을 덮도록 리플레이 녹화만 새로 시작한다.
|
|
1814
|
+
onScreenRulesChanged: () => void this.sessionReplayController.restartRecording(),
|
|
1815
|
+
recapturesScreenshots: false
|
|
1816
|
+
});
|
|
1817
|
+
created = withExclusions(createdIssue, { screenshotExcluded });
|
|
1818
|
+
await enqueueIssueUploads({
|
|
1819
|
+
videoQueue: this.uploadQueues.video,
|
|
1820
|
+
attachmentQueue: this.uploadQueues.attachments,
|
|
1821
|
+
issueId: createdIssue.id,
|
|
1822
|
+
projectKey: this.state.requireProjectId(),
|
|
1823
|
+
video: null,
|
|
1824
|
+
attachments
|
|
1592
1825
|
});
|
|
1593
1826
|
this.state.setSnapshot({
|
|
1594
1827
|
issues: [createdIssue, ...this.snapshot.issues]
|
|
1595
1828
|
});
|
|
1596
|
-
return
|
|
1829
|
+
return created;
|
|
1597
1830
|
} catch (error) {
|
|
1598
|
-
|
|
1831
|
+
if (created) {
|
|
1832
|
+
console.warn(
|
|
1833
|
+
ISSUE_STICKER_INTERNAL_LOG_PREFIX,
|
|
1834
|
+
"[headless] createIssue: 이슈를 생성한 뒤 후속 처리에 실패했습니다.",
|
|
1835
|
+
error
|
|
1836
|
+
);
|
|
1837
|
+
return created;
|
|
1838
|
+
}
|
|
1839
|
+
const replay = this.submitReplay();
|
|
1840
|
+
const mapped = toHeadlessMaskingError(error, {
|
|
1841
|
+
onScreenRulesChanged: () => replay.restartRecording(),
|
|
1842
|
+
replayRecording: replay.isRecording(),
|
|
1843
|
+
recapturesScreenshots: false,
|
|
1844
|
+
restartReplay: () => replay.restartRecording()
|
|
1845
|
+
});
|
|
1846
|
+
return this.skipCreateIssue(this.toHeadlessError(mapped), onError);
|
|
1599
1847
|
}
|
|
1600
1848
|
}
|
|
1601
1849
|
/**
|
|
1602
|
-
* createIssue 가 이슈를 만들지 않고 끝난 이유를
|
|
1850
|
+
* createIssue 가 이슈를 만들지 않고 끝난 이유를 남기고 onError 로 넘긴다.
|
|
1603
1851
|
*
|
|
1604
1852
|
* devLogger 는 프로덕션 빌드에서 침묵하는데, 이 실패는 예외로 드러나지 않으므로
|
|
1605
1853
|
* 통합한 쪽이 프로덕션에서도 알아야 한다. 접두어가 붙어 있어 우리 수집기가
|
|
1606
1854
|
* 자기 로그로 걸러내므로 고객 이슈의 콘솔 로그를 오염시키지 않는다.
|
|
1855
|
+
* onError 가 던져도 createIssue 는 던지지 않는다 — 전역 핸들러 안에서 부른 경우 루프가 된다.
|
|
1607
1856
|
*/
|
|
1608
|
-
skipCreateIssue(
|
|
1609
|
-
const args = [
|
|
1610
|
-
|
|
1857
|
+
skipCreateIssue(error, onError) {
|
|
1858
|
+
const args = [
|
|
1859
|
+
ISSUE_STICKER_INTERNAL_LOG_PREFIX,
|
|
1860
|
+
"[headless] createIssue:",
|
|
1861
|
+
error.message
|
|
1862
|
+
];
|
|
1863
|
+
if (error.cause !== void 0) args.push(error.cause);
|
|
1611
1864
|
console.warn(...args);
|
|
1865
|
+
try {
|
|
1866
|
+
onError == null ? void 0 : onError(error);
|
|
1867
|
+
} catch (callbackError) {
|
|
1868
|
+
console.warn(
|
|
1869
|
+
ISSUE_STICKER_INTERNAL_LOG_PREFIX,
|
|
1870
|
+
"[headless] createIssue onError:",
|
|
1871
|
+
callbackError
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1612
1874
|
return null;
|
|
1613
1875
|
}
|
|
1614
1876
|
async loginAsMember() {
|
|
@@ -1642,6 +1904,8 @@ class HeadlessClientImpl {
|
|
|
1642
1904
|
void this.sessionReplayController.refresh();
|
|
1643
1905
|
}
|
|
1644
1906
|
destroy() {
|
|
1907
|
+
this.destroyed = true;
|
|
1908
|
+
this.uploadQueues.stop();
|
|
1645
1909
|
this.clearCaptureListeners();
|
|
1646
1910
|
this.suppressNextVideoStopSelection = true;
|
|
1647
1911
|
this.captureSession = null;
|
|
@@ -1651,8 +1915,35 @@ class HeadlessClientImpl {
|
|
|
1651
1915
|
this.logCollectors.stop();
|
|
1652
1916
|
this.autoIssueRateLimit.reset();
|
|
1653
1917
|
this.authBridge.destroy();
|
|
1918
|
+
this.maskingPolicy = this.createMaskingPolicy();
|
|
1919
|
+
this.maskingPolicyPrefetch = null;
|
|
1920
|
+
this.maskingPolicyFetchedAt = null;
|
|
1654
1921
|
this.state.reset();
|
|
1655
1922
|
}
|
|
1923
|
+
createMaskingPolicy() {
|
|
1924
|
+
const inner = createMaskingPolicySource({
|
|
1925
|
+
// 제출 직전 재확인이 브라우저 캐시의 옛 버전을 받으면 409 를 부른다.
|
|
1926
|
+
fetchSnapshot: () => this.transport.request("/api/sdk/masking-policy", { cache: "no-store" }),
|
|
1927
|
+
cache: createMemoryMaskingPolicyCache()
|
|
1928
|
+
});
|
|
1929
|
+
const source = {
|
|
1930
|
+
getMaskingPolicySnapshot: () => inner.getMaskingPolicySnapshot(),
|
|
1931
|
+
refreshMaskingPolicySnapshot: async (options) => {
|
|
1932
|
+
const snapshot = await inner.refreshMaskingPolicySnapshot(options);
|
|
1933
|
+
if (this.maskingPolicy === source) this.maskingPolicyFetchedAt = Date.now();
|
|
1934
|
+
return snapshot;
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
return source;
|
|
1938
|
+
}
|
|
1939
|
+
/** 정책을 미리 받는다. 녹화 시작은 캐시가 없으면 이 요청을 기다린다. */
|
|
1940
|
+
prefetchMaskingPolicy() {
|
|
1941
|
+
const prefetch = this.maskingPolicy.refreshMaskingPolicySnapshot({
|
|
1942
|
+
timeoutMs: MASKING_POLICY_PREFETCH_TIMEOUT_MS
|
|
1943
|
+
});
|
|
1944
|
+
prefetch.catch(() => void 0);
|
|
1945
|
+
this.maskingPolicyPrefetch = prefetch;
|
|
1946
|
+
}
|
|
1656
1947
|
async initializeInternal() {
|
|
1657
1948
|
this.state.setSnapshot({ phase: "initializing", error: null });
|
|
1658
1949
|
try {
|
|
@@ -1675,6 +1966,12 @@ class HeadlessClientImpl {
|
|
|
1675
1966
|
filterSchema: { groups: [], unavailableReason: "public-mode" }
|
|
1676
1967
|
});
|
|
1677
1968
|
this.logCollectors.start();
|
|
1969
|
+
this.prefetchMaskingPolicy();
|
|
1970
|
+
const { video, attachments } = this.uploadQueues;
|
|
1971
|
+
void this.uploadQueues.resume([result.projectId]).then(() => {
|
|
1972
|
+
void video.pruneExpired();
|
|
1973
|
+
void attachments.pruneExpired();
|
|
1974
|
+
});
|
|
1678
1975
|
this.authBridge.mountHiddenBridgeFrame();
|
|
1679
1976
|
void this.sessionReplayController.initialize();
|
|
1680
1977
|
} catch (error) {
|
|
@@ -1687,28 +1984,97 @@ class HeadlessClientImpl {
|
|
|
1687
1984
|
const sessionReplay = this.sessionReplayController.getSnapshot();
|
|
1688
1985
|
if (!sessionReplay.enabled || !sessionReplay.canAttach) return null;
|
|
1689
1986
|
try {
|
|
1690
|
-
const snapshot = await this.sessionReplayController.takeSnapshot();
|
|
1691
|
-
return snapshot ?
|
|
1987
|
+
const snapshot = await this.sessionReplayController.takeSnapshot([]);
|
|
1988
|
+
return snapshot ? toSessionReplayMeta(snapshot) : null;
|
|
1692
1989
|
} catch {
|
|
1693
1990
|
return null;
|
|
1694
1991
|
}
|
|
1695
1992
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
const
|
|
1993
|
+
/**
|
|
1994
|
+
* 제출이 사용하는 리플레이 녹화. 꺼졌거나 한도를 넘으면 메모와 스냅샷이 null 이다. 스냅샷의
|
|
1995
|
+
* 마스킹 실패는 삼키지 않는다 — submitDraft 가 이슈 생성을 멈추고 CAPTURE_FAILED 로 바꾼다.
|
|
1996
|
+
*/
|
|
1997
|
+
submitReplay() {
|
|
1998
|
+
const controller = this.sessionReplayController;
|
|
1999
|
+
const attached = () => {
|
|
2000
|
+
const sessionReplay = controller.getSnapshot();
|
|
2001
|
+
return sessionReplay.enabled && sessionReplay.canAttach;
|
|
2002
|
+
};
|
|
2003
|
+
return {
|
|
2004
|
+
whenRestarted: () => controller.whenRestarted(),
|
|
2005
|
+
getMaskMemoSelectors: () => attached() ? controller.getMaskMemoSelectors() : null,
|
|
2006
|
+
takeSnapshot: async (maskSelectors) => attached() ? controller.takeSnapshot(maskSelectors) : null,
|
|
2007
|
+
isRecording: () => controller.isRecording(),
|
|
2008
|
+
restartRecording: () => void controller.restartRecording()
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* 다른 워크스페이스·프로젝트 컨텍스트로 바뀌면 정책을 새로 받고, 그 정책 선택자로 리플레이 녹화를
|
|
2013
|
+
* 새로 시작한다.
|
|
2014
|
+
*/
|
|
2015
|
+
applyBridgeToken(result) {
|
|
2016
|
+
const { tenantId, projectId } = this.snapshot.auth;
|
|
2017
|
+
this.state.applyBridgeToken(result);
|
|
2018
|
+
const next = this.snapshot.auth;
|
|
2019
|
+
if (tenantId !== null && (next.tenantId !== tenantId || next.projectId !== projectId)) {
|
|
2020
|
+
this.maskingPolicy = this.createMaskingPolicy();
|
|
2021
|
+
this.maskingPolicyFetchedAt = null;
|
|
2022
|
+
this.prefetchMaskingPolicy();
|
|
2023
|
+
void this.sessionReplayController.restartRecording();
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* 스티커 목록에는 영상 URL 도 업로드를 포기한 까닭도 없다. 업로드가 끝날 때 받아 둔 값을 같은
|
|
2028
|
+
* 이슈에 남긴다. 까닭은 여전히 실패 상태일 때만 남긴다.
|
|
2029
|
+
*/
|
|
2030
|
+
keepKnownVideos(issues) {
|
|
2031
|
+
const known = new Map(this.snapshot.issues.map((issue) => [issue.id, issue]));
|
|
2032
|
+
return issues.map((issue) => {
|
|
2033
|
+
const previous = known.get(issue.id);
|
|
2034
|
+
const video = !issue.video ? previous == null ? void 0 : previous.video : void 0;
|
|
2035
|
+
const reason = issue.video_status === "failed" ? previous == null ? void 0 : previous.video_failure_reason : void 0;
|
|
2036
|
+
if (!video && !reason) return issue;
|
|
1704
2037
|
return {
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
2038
|
+
...issue,
|
|
2039
|
+
...video ? { video } : {},
|
|
2040
|
+
...reason ? { video_failure_reason: reason } : {}
|
|
1708
2041
|
};
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* 업로드가 끝나면 목록·상세의 같은 이슈에 영상 상태를 반영한다. 완료는 재조회해 영상 URL 까지
|
|
2046
|
+
* 채우고, 재조회가 안 되면(config.user 없는 게스트 등) 상태만 고친다.
|
|
2047
|
+
*/
|
|
2048
|
+
async handleVideoUploadEvent(event) {
|
|
2049
|
+
if (event.type === "failed" || this.destroyed) return;
|
|
2050
|
+
const refreshed = event.type === "completed" ? await this.issueRepository.openIssue(event.issueId).catch(() => null) : null;
|
|
2051
|
+
if (this.destroyed) return;
|
|
2052
|
+
const videoStatus = event.type === "completed" ? "ready" : "failed";
|
|
2053
|
+
const failure = event.reason ? { video_failure_reason: event.reason } : {};
|
|
2054
|
+
const { issues, activeIssue } = this.snapshot;
|
|
2055
|
+
this.state.setSnapshot({
|
|
2056
|
+
issues: issues.map(
|
|
2057
|
+
(issue) => issue.id === event.issueId ? {
|
|
2058
|
+
...issue,
|
|
2059
|
+
video: (refreshed == null ? void 0 : refreshed.video) ?? issue.video,
|
|
2060
|
+
video_status: videoStatus,
|
|
2061
|
+
...failure
|
|
2062
|
+
} : issue
|
|
2063
|
+
),
|
|
2064
|
+
activeIssue: (activeIssue == null ? void 0 : activeIssue.id) === event.issueId ? refreshed ?? { ...activeIssue, video_status: videoStatus, ...failure } : activeIssue
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
/** 리플레이가 연결되거나 포기되면 목록·상세의 같은 이슈에 상태를 반영한다. 소켓 전사는 상태가 없다. */
|
|
2068
|
+
handleAttachmentUploadEvent(event) {
|
|
2069
|
+
if (event.mediaType !== "replay" || event.type === "failed" || this.destroyed) return;
|
|
2070
|
+
const replayStatus = event.type === "completed" ? "ready" : "failed";
|
|
2071
|
+
const { issues, activeIssue } = this.snapshot;
|
|
2072
|
+
this.state.setSnapshot({
|
|
2073
|
+
issues: issues.map(
|
|
2074
|
+
(issue) => issue.id === event.issueId ? { ...issue, replay_status: replayStatus } : issue
|
|
2075
|
+
),
|
|
2076
|
+
activeIssue: (activeIssue == null ? void 0 : activeIssue.id) === event.issueId ? { ...activeIssue, replay_status: replayStatus } : activeIssue
|
|
2077
|
+
});
|
|
1712
2078
|
}
|
|
1713
2079
|
async handleRecordedVideoStopped(file) {
|
|
1714
2080
|
if (this.suppressNextVideoStopSelection) {
|
|
@@ -1778,15 +2144,16 @@ class HeadlessClientImpl {
|
|
|
1778
2144
|
this.beginBackgroundCapturePreparation(draft, target);
|
|
1779
2145
|
}
|
|
1780
2146
|
beginBackgroundCapturePreparation(draft, target) {
|
|
1781
|
-
var _a
|
|
2147
|
+
var _a;
|
|
1782
2148
|
const token = {};
|
|
1783
2149
|
this.captureSession = {
|
|
1784
2150
|
token,
|
|
2151
|
+
target,
|
|
1785
2152
|
screenshots: draft.mediaMode === "screenshot" ? (
|
|
1786
|
-
//
|
|
1787
|
-
//
|
|
1788
|
-
this.
|
|
1789
|
-
() =>
|
|
2153
|
+
// 영역은 초기화 때 받아 둔 정책 캐시의 선택자로 계산한다. 캐시가 없으면 빈 목록이고,
|
|
2154
|
+
// 미리보기를 칠할 때 덮개 검사에 걸려 한 번 다시 찍는다.
|
|
2155
|
+
loadCaptureSelectors(() => this.livePolicySource.getMaskingPolicySnapshot()).then(
|
|
2156
|
+
(selectors) => this.captureScreenshots(target, selectors ?? [])
|
|
1790
2157
|
)
|
|
1791
2158
|
) : null,
|
|
1792
2159
|
formSchema: this.loadFormSchema({})
|
|
@@ -1795,11 +2162,10 @@ class HeadlessClientImpl {
|
|
|
1795
2162
|
var _a2;
|
|
1796
2163
|
return ((_a2 = this.captureSession) == null ? void 0 : _a2.token) === token;
|
|
1797
2164
|
};
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
void ((_b = this.captureSession.formSchema) == null ? void 0 : _b.then((formSchema) => {
|
|
2165
|
+
if (this.captureSession.screenshots) {
|
|
2166
|
+
this.publishPreview(this.captureSession.screenshots, true);
|
|
2167
|
+
}
|
|
2168
|
+
void ((_a = this.captureSession.formSchema) == null ? void 0 : _a.then((formSchema) => {
|
|
1803
2169
|
if (!isCurrentSession()) return;
|
|
1804
2170
|
this.state.setSnapshot({ formSchema });
|
|
1805
2171
|
}));
|
|
@@ -1812,6 +2178,64 @@ class HeadlessClientImpl {
|
|
|
1812
2178
|
});
|
|
1813
2179
|
}
|
|
1814
2180
|
}
|
|
2181
|
+
/**
|
|
2182
|
+
* 화면 규칙이 바뀌어 지금 원본을 쓸 수 없다. 같은 대상을 다시 찍어 미리보기를 바꾼다.
|
|
2183
|
+
*
|
|
2184
|
+
* 선택자는 캐시가 아니라 제출과 같은 방식으로 다시 받아 읽는다. 받은 스냅샷의 캐시 쓰기는
|
|
2185
|
+
* 기다리지 않고 떠나므로 캐시만 읽으면 바뀌기 전 선택자로 찍을 수 있다. 정책을 받지 못하면
|
|
2186
|
+
* 스크린샷 없이 두고, 마스킹 정책 때문에 버렸다고 표시해 제출 때 알린다.
|
|
2187
|
+
*/
|
|
2188
|
+
recaptureScreenshots(autoRecapture = true) {
|
|
2189
|
+
const session = this.captureSession;
|
|
2190
|
+
if (!(session == null ? void 0 : session.screenshots)) return;
|
|
2191
|
+
const screenshots = this.freshCaptureSelectors().then(
|
|
2192
|
+
(selectors) => this.captureScreenshots(session.target, selectors),
|
|
2193
|
+
() => ({ screenshots: [], maskingDropped: true })
|
|
2194
|
+
);
|
|
2195
|
+
this.captureSession = { ...session, screenshots };
|
|
2196
|
+
this.mergeDraft({ screenshotPreviewUrls: [] });
|
|
2197
|
+
this.publishPreview(screenshots, autoRecapture);
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* 원본이 준비되면 칠한 미리보기를 draft 에 싣는다. 칠하지 못하면 원본을 내보내지 않고 비워 둔다.
|
|
2201
|
+
*
|
|
2202
|
+
* 덮개 검사에 걸리면(캐시 없이 빈 선택자로 찍었거나 그 사이 정책이 바뀌었다) 그대로 두면 첫 제출이
|
|
2203
|
+
* 반드시 실패하므로 같은 대상을 한 번 다시 찍는다. 다시 찍은 캡처는 또 다시 찍지 않는다.
|
|
2204
|
+
*/
|
|
2205
|
+
publishPreview(screenshots, autoRecapture) {
|
|
2206
|
+
const isCurrent = () => {
|
|
2207
|
+
var _a;
|
|
2208
|
+
return ((_a = this.captureSession) == null ? void 0 : _a.screenshots) === screenshots;
|
|
2209
|
+
};
|
|
2210
|
+
void screenshots.then((captured) => this.previewScreenshots(captured.screenshots)).then(
|
|
2211
|
+
(screenshotPreviewUrls) => {
|
|
2212
|
+
if (isCurrent()) this.mergeDraft({ screenshotPreviewUrls });
|
|
2213
|
+
},
|
|
2214
|
+
(error) => {
|
|
2215
|
+
if (isCurrent() && autoRecapture && error instanceof MaskingScreenRulesChangedError) {
|
|
2216
|
+
this.recaptureScreenshots(false);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
/** 제출과 같은 방식(짧게 다시 받고 실패하면 캐시)으로 읽은 정책 선택자. 둘 다 없으면 실패한다. */
|
|
2222
|
+
async freshCaptureSelectors() {
|
|
2223
|
+
return collectSnapshotSelectors(await loadMaskingPolicyForSubmit(this.livePolicySource));
|
|
2224
|
+
}
|
|
2225
|
+
captureScreenshots(target, maskSelectors) {
|
|
2226
|
+
return this.logCollectors.pauseNetworkWhile(
|
|
2227
|
+
() => captureHeadlessScreenshots({
|
|
2228
|
+
target,
|
|
2229
|
+
excludeSelectors: this.getExcludeSelectors(),
|
|
2230
|
+
maskSelectors
|
|
2231
|
+
})
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
async previewScreenshots(shots) {
|
|
2235
|
+
const snapshot = await loadMaskingPolicyForSubmit(this.livePolicySource);
|
|
2236
|
+
const rules = resolveEffectivePolicy(snapshot, this.state.requireProjectId());
|
|
2237
|
+
return maskScreenshotsForSubmit(shots, rules);
|
|
2238
|
+
}
|
|
1815
2239
|
mergeDraft(patch) {
|
|
1816
2240
|
const draft = this.snapshot.draft;
|
|
1817
2241
|
if (!draft) return;
|