issue-sticker 0.12.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 +46 -0
- package/dist/{externalUserPayload-DWcXyz2U.js → externalUserPayload-CdzgCq4t.js} +10453 -7486
- package/dist/issue-sticker-headless-react.js +68 -12
- package/dist/issue-sticker-headless.js +457 -95
- package/dist/issue-sticker-react.js +1431 -400
- package/dist/{three-B228j4M6.js → three-FamoZZKK.js} +560 -33
- package/dist/{tooltip-CT7e9xbU.js → tooltip-BVA6ikuJ.js} +1 -1
- package/headless.d.ts +35 -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;
|
|
@@ -99,7 +100,8 @@ class HeadlessTransport {
|
|
|
99
100
|
const execute = (nextHeaders) => pristineFetch(`${getApiBaseUrl()}${endpoint}`, {
|
|
100
101
|
method: options.method ?? "GET",
|
|
101
102
|
headers: nextHeaders,
|
|
102
|
-
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 } : {}
|
|
103
105
|
});
|
|
104
106
|
let response = await execute(headers);
|
|
105
107
|
if (!options.skipAuth && response.status === 401 && this.options.getAuthMode() === "member") {
|
|
@@ -115,8 +117,9 @@ class HeadlessTransport {
|
|
|
115
117
|
}
|
|
116
118
|
const json = await response.json().catch(() => null);
|
|
117
119
|
if (!response.ok || !(json == null ? void 0 : json.success)) {
|
|
120
|
+
const unauthorizedCode = this.options.getAuthMode() === "member" ? "MEMBER_AUTH_REQUIRED" : "AUTH_FAILED";
|
|
118
121
|
throw new IssueStickerHeadlessError(
|
|
119
|
-
response.status === 401 ?
|
|
122
|
+
response.status === 401 ? unauthorizedCode : "NETWORK_ERROR",
|
|
120
123
|
extractErrorMessage(json == null ? void 0 : json.error, (json == null ? void 0 : json.message) || `요청 실패: ${response.status}`),
|
|
121
124
|
json,
|
|
122
125
|
response.status
|
|
@@ -473,21 +476,34 @@ function buildHeadlessCaptureContext({
|
|
|
473
476
|
sessionReplayPreview: null
|
|
474
477
|
};
|
|
475
478
|
}
|
|
479
|
+
const NO_HEADLESS_SCREENSHOTS = {
|
|
480
|
+
screenshots: [],
|
|
481
|
+
maskingDropped: false
|
|
482
|
+
};
|
|
476
483
|
async function captureHeadlessScreenshots({
|
|
477
484
|
target,
|
|
478
|
-
excludeSelectors
|
|
485
|
+
excludeSelectors,
|
|
486
|
+
maskSelectors
|
|
479
487
|
}) {
|
|
480
488
|
const targetIsDocumentRoot = target === document.body || target === document.documentElement;
|
|
481
|
-
const
|
|
489
|
+
const viewport = await captureViewportAndElementShots({
|
|
482
490
|
element: target,
|
|
483
|
-
excludeSelectors
|
|
491
|
+
excludeSelectors,
|
|
492
|
+
maskSelectors
|
|
484
493
|
});
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
)
|
|
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
|
+
};
|
|
491
507
|
}
|
|
492
508
|
function normalizeCustomInputType(fieldType) {
|
|
493
509
|
if (fieldType === "number") return "number";
|
|
@@ -1087,7 +1103,8 @@ function buildIssueCreatePayload({
|
|
|
1087
1103
|
uploads,
|
|
1088
1104
|
logs,
|
|
1089
1105
|
currentUrl,
|
|
1090
|
-
userAgent
|
|
1106
|
+
userAgent,
|
|
1107
|
+
maskingPolicyVersion
|
|
1091
1108
|
}) {
|
|
1092
1109
|
var _a, _b, _c;
|
|
1093
1110
|
const title = (_a = draft.title) == null ? void 0 : _a.trim();
|
|
@@ -1107,6 +1124,7 @@ function buildIssueCreatePayload({
|
|
|
1107
1124
|
tenant_id: auth.tenantId,
|
|
1108
1125
|
project_id: projectId,
|
|
1109
1126
|
account_id: auth.accountId ?? void 0,
|
|
1127
|
+
masking_policy_version: maskingPolicyVersion,
|
|
1110
1128
|
...buildExternalUserPayload(config.user)
|
|
1111
1129
|
};
|
|
1112
1130
|
const { networkLogs, stillOverBudget } = shrinkNetworkLogsToFit(
|
|
@@ -1121,8 +1139,67 @@ function buildIssueCreatePayload({
|
|
|
1121
1139
|
stillOverBudget
|
|
1122
1140
|
};
|
|
1123
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
|
+
}
|
|
1124
1200
|
async function submitHeadlessIssueDraft({
|
|
1125
1201
|
draft,
|
|
1202
|
+
target,
|
|
1126
1203
|
pendingVideoFile,
|
|
1127
1204
|
transport,
|
|
1128
1205
|
issueRepository,
|
|
@@ -1133,39 +1210,104 @@ async function submitHeadlessIssueDraft({
|
|
|
1133
1210
|
logs,
|
|
1134
1211
|
currentUrl,
|
|
1135
1212
|
userAgent,
|
|
1136
|
-
|
|
1137
|
-
|
|
1213
|
+
screenshots,
|
|
1214
|
+
screenshotsDroppedByMasking,
|
|
1215
|
+
sessionReplay,
|
|
1216
|
+
getSocketConnections,
|
|
1217
|
+
maskingPolicy,
|
|
1218
|
+
onScreenRulesChanged,
|
|
1219
|
+
recapturesScreenshots
|
|
1138
1220
|
}) {
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1141
|
-
const attachments = await collectAttachmentUploads({
|
|
1142
|
-
takeReplay: draft.sessionReplayEnabled ? takeSessionReplaySnapshot : void 0,
|
|
1143
|
-
getConnections: getSocketConnections
|
|
1144
|
-
});
|
|
1145
|
-
const { payload: issueData, stillOverBudget } = buildIssueCreatePayload({
|
|
1146
|
-
draft,
|
|
1147
|
-
auth,
|
|
1148
|
-
config,
|
|
1149
|
-
projectId,
|
|
1150
|
-
uploads: {
|
|
1151
|
-
screenshotUploadIds,
|
|
1152
|
-
videoPending: draft.mediaMode === "video" && Boolean(pendingVideoFile),
|
|
1153
|
-
replayPending: hasReplayUpload(attachments)
|
|
1154
|
-
},
|
|
1155
|
-
logs,
|
|
1156
|
-
currentUrl,
|
|
1157
|
-
userAgent
|
|
1158
|
-
});
|
|
1159
|
-
if (stillOverBudget) {
|
|
1160
|
-
throw new IssueStickerHeadlessError(
|
|
1161
|
-
"PAYLOAD_TOO_LARGE",
|
|
1162
|
-
"이슈 데이터가 전송 한도를 넘어 저장할 수 없습니다. 설명을 줄이거나 첨부를 빼고 다시 시도해주세요."
|
|
1163
|
-
);
|
|
1164
|
-
}
|
|
1221
|
+
const hasVideo = draft.mediaMode === "video" && Boolean(pendingVideoFile);
|
|
1222
|
+
const socketConnections = getSocketConnections();
|
|
1165
1223
|
const integrations = await issueRepository.buildIntegrationsPayload(draft, formSchema);
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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;
|
|
1169
1311
|
}
|
|
1170
1312
|
const HEADLESS_CAPTURE_STYLE_ID = "issue-sticker-headless-capture-style";
|
|
1171
1313
|
const HEADLESS_CAPTURE_STYLE = `
|
|
@@ -1272,6 +1414,13 @@ function toHeadlessAttachmentUpload(state) {
|
|
|
1272
1414
|
progress: state.bytesTotal > 0 ? state.bytesSent / state.bytesTotal : 0
|
|
1273
1415
|
};
|
|
1274
1416
|
}
|
|
1417
|
+
function withExclusions(issue, flags) {
|
|
1418
|
+
return {
|
|
1419
|
+
...issue,
|
|
1420
|
+
...flags.videoExcluded ? { videoExcluded: true } : {},
|
|
1421
|
+
...flags.screenshotExcluded ? { screenshotExcluded: true } : {}
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1275
1424
|
class HeadlessClientImpl {
|
|
1276
1425
|
constructor(config) {
|
|
1277
1426
|
var _a;
|
|
@@ -1282,6 +1431,12 @@ class HeadlessClientImpl {
|
|
|
1282
1431
|
this.initializePromise = null;
|
|
1283
1432
|
this.captureCleanup = null;
|
|
1284
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;
|
|
1285
1440
|
this.suppressNextVideoStopSelection = false;
|
|
1286
1441
|
this.destroyed = false;
|
|
1287
1442
|
if (!((_a = config.apiKey) == null ? void 0 : _a.trim())) {
|
|
@@ -1291,12 +1446,13 @@ class HeadlessClientImpl {
|
|
|
1291
1446
|
apiKey: config.apiKey,
|
|
1292
1447
|
getTenantId: () => this.snapshot.auth.tenantId,
|
|
1293
1448
|
getProjectId: () => this.snapshot.auth.projectId,
|
|
1294
|
-
onToken: (result) => this.
|
|
1449
|
+
onToken: (result) => this.applyBridgeToken(result)
|
|
1295
1450
|
});
|
|
1296
1451
|
this.transport = new HeadlessTransport({
|
|
1297
1452
|
getAuthMode: () => this.snapshot.auth.mode,
|
|
1298
1453
|
resolveAuthorizationHeader: (forceRefreshMemberToken) => this.resolveAuthorizationHeader(forceRefreshMemberToken)
|
|
1299
1454
|
});
|
|
1455
|
+
this.maskingPolicy = this.createMaskingPolicy();
|
|
1300
1456
|
this.issueRepository = new HeadlessIssueRepository({
|
|
1301
1457
|
transport: this.transport,
|
|
1302
1458
|
getProjectId: () => this.state.requireProjectId(),
|
|
@@ -1339,7 +1495,14 @@ class HeadlessClientImpl {
|
|
|
1339
1495
|
});
|
|
1340
1496
|
});
|
|
1341
1497
|
this.sessionReplayController = new SessionReplayCaptureController({
|
|
1342
|
-
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
|
|
1343
1506
|
});
|
|
1344
1507
|
this.sessionReplayController.subscribe((sessionReplay) => {
|
|
1345
1508
|
this.applySessionReplaySnapshot(sessionReplay);
|
|
@@ -1511,19 +1674,24 @@ class HeadlessClientImpl {
|
|
|
1511
1674
|
this.state.setSnapshot({ phase: "submittingIssue", error: null });
|
|
1512
1675
|
try {
|
|
1513
1676
|
const session = this.captureSession;
|
|
1514
|
-
const
|
|
1677
|
+
const captured = (session == null ? void 0 : session.screenshots) ? await session.screenshots : NO_HEADLESS_SCREENSHOTS;
|
|
1515
1678
|
const formSchema = this.snapshot.formSchema ?? ((session == null ? void 0 : session.formSchema) ? await session.formSchema : null);
|
|
1516
1679
|
const submission = {
|
|
1517
1680
|
...this.state.requireDraft(),
|
|
1518
|
-
screenshotPreviewUrls,
|
|
1519
1681
|
title: input.title,
|
|
1520
1682
|
description: input.description,
|
|
1521
1683
|
intent: input.intent,
|
|
1522
1684
|
dynamicFields: input.dynamicFields ?? {}
|
|
1523
1685
|
};
|
|
1524
1686
|
const videoFile = submission.mediaMode === "video" ? this.pendingMedia.videoFile : null;
|
|
1525
|
-
const {
|
|
1687
|
+
const {
|
|
1688
|
+
issue: createdIssue,
|
|
1689
|
+
attachments,
|
|
1690
|
+
videoExcluded,
|
|
1691
|
+
screenshotExcluded
|
|
1692
|
+
} = await submitHeadlessIssueDraft({
|
|
1526
1693
|
draft: submission,
|
|
1694
|
+
target: (session == null ? void 0 : session.target) ?? null,
|
|
1527
1695
|
pendingVideoFile: this.pendingMedia.videoFile,
|
|
1528
1696
|
transport: this.transport,
|
|
1529
1697
|
issueRepository: this.issueRepository,
|
|
@@ -1534,15 +1702,26 @@ class HeadlessClientImpl {
|
|
|
1534
1702
|
logs: this.logCollectors.collect(),
|
|
1535
1703
|
currentUrl: getCurrentPageUrl(),
|
|
1536
1704
|
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
1537
|
-
|
|
1538
|
-
|
|
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)
|
|
1539
1718
|
});
|
|
1540
1719
|
await enqueueIssueUploads({
|
|
1541
1720
|
videoQueue: this.uploadQueues.video,
|
|
1542
1721
|
attachmentQueue: this.uploadQueues.attachments,
|
|
1543
1722
|
issueId: createdIssue.id,
|
|
1544
1723
|
projectKey: this.state.requireProjectId(),
|
|
1545
|
-
video: videoFile ? { blob: videoFile, mimeType: normalizeUploadMimeType(videoFile.type, "video") } : null,
|
|
1724
|
+
video: videoFile && !videoExcluded ? { blob: videoFile, mimeType: normalizeUploadMimeType(videoFile.type, "video") } : null,
|
|
1546
1725
|
attachments
|
|
1547
1726
|
});
|
|
1548
1727
|
this.clearCaptureListeners();
|
|
@@ -1554,7 +1733,7 @@ class HeadlessClientImpl {
|
|
|
1554
1733
|
formSchema: null,
|
|
1555
1734
|
issues: [createdIssue, ...this.snapshot.issues]
|
|
1556
1735
|
});
|
|
1557
|
-
return createdIssue;
|
|
1736
|
+
return withExclusions(createdIssue, { videoExcluded, screenshotExcluded });
|
|
1558
1737
|
} catch (error) {
|
|
1559
1738
|
const nextError = this.toHeadlessError(error);
|
|
1560
1739
|
this.state.setSnapshot({ phase: "draftingIssue", error: nextError });
|
|
@@ -1566,17 +1745,32 @@ class HeadlessClientImpl {
|
|
|
1566
1745
|
}
|
|
1567
1746
|
async createIssue(input) {
|
|
1568
1747
|
var _a, _b;
|
|
1748
|
+
const onError = input == null ? void 0 : input.onError;
|
|
1749
|
+
let created = null;
|
|
1569
1750
|
try {
|
|
1570
|
-
if (!((_a = input.title) == null ? void 0 : _a.trim()) || !((_b = input.description) == null ? void 0 : _b.trim())) {
|
|
1571
|
-
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
|
+
);
|
|
1572
1756
|
}
|
|
1573
1757
|
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
1574
|
-
return this.skipCreateIssue(
|
|
1758
|
+
return this.skipCreateIssue(
|
|
1759
|
+
new IssueStickerHeadlessError(
|
|
1760
|
+
"VALIDATION_ERROR",
|
|
1761
|
+
"브라우저 DOM 에서만 이슈를 만들 수 있습니다."
|
|
1762
|
+
),
|
|
1763
|
+
onError
|
|
1764
|
+
);
|
|
1575
1765
|
}
|
|
1576
1766
|
await this.ensureInitialized();
|
|
1577
1767
|
if (!this.autoIssueRateLimit.claim()) {
|
|
1578
1768
|
return this.skipCreateIssue(
|
|
1579
|
-
|
|
1769
|
+
new IssueStickerHeadlessError(
|
|
1770
|
+
"RATE_LIMITED",
|
|
1771
|
+
`${this.autoIssueRateLimit.label}까지 호출할 수 있습니다. 호출 조건을 다시 확인해주세요.`
|
|
1772
|
+
),
|
|
1773
|
+
onError
|
|
1580
1774
|
);
|
|
1581
1775
|
}
|
|
1582
1776
|
const target = input.target ?? document.body;
|
|
@@ -1586,18 +1780,21 @@ class HeadlessClientImpl {
|
|
|
1586
1780
|
sessionReplayEnabled: Boolean(sessionReplay.enabled && sessionReplay.canAttach)
|
|
1587
1781
|
});
|
|
1588
1782
|
const logs = this.logCollectors.collect();
|
|
1589
|
-
const
|
|
1590
|
-
() => captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() })
|
|
1591
|
-
);
|
|
1783
|
+
const captured = input.media === "none" ? NO_HEADLESS_SCREENSHOTS : await this.captureScreenshots(target, await this.freshCaptureSelectors());
|
|
1592
1784
|
const formSchema = this.snapshot.auth.mode === "member" && Object.keys(input.dynamicFields ?? {}).length > 0 ? await this.loadFormSchema({}) : null;
|
|
1593
|
-
const {
|
|
1785
|
+
const {
|
|
1786
|
+
issue: createdIssue,
|
|
1787
|
+
attachments,
|
|
1788
|
+
screenshotExcluded
|
|
1789
|
+
} = await submitHeadlessIssueDraft({
|
|
1594
1790
|
draft: {
|
|
1595
1791
|
...draft,
|
|
1596
|
-
screenshotPreviewUrls,
|
|
1792
|
+
screenshotPreviewUrls: [],
|
|
1597
1793
|
title: input.title,
|
|
1598
1794
|
description: input.description,
|
|
1599
1795
|
dynamicFields: input.dynamicFields ?? {}
|
|
1600
1796
|
},
|
|
1797
|
+
target,
|
|
1601
1798
|
pendingVideoFile: null,
|
|
1602
1799
|
transport: this.transport,
|
|
1603
1800
|
issueRepository: this.issueRepository,
|
|
@@ -1608,9 +1805,16 @@ class HeadlessClientImpl {
|
|
|
1608
1805
|
logs,
|
|
1609
1806
|
currentUrl: getCurrentPageUrl(),
|
|
1610
1807
|
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
1611
|
-
|
|
1612
|
-
|
|
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
|
|
1613
1816
|
});
|
|
1817
|
+
created = withExclusions(createdIssue, { screenshotExcluded });
|
|
1614
1818
|
await enqueueIssueUploads({
|
|
1615
1819
|
videoQueue: this.uploadQueues.video,
|
|
1616
1820
|
attachmentQueue: this.uploadQueues.attachments,
|
|
@@ -1622,22 +1826,51 @@ class HeadlessClientImpl {
|
|
|
1622
1826
|
this.state.setSnapshot({
|
|
1623
1827
|
issues: [createdIssue, ...this.snapshot.issues]
|
|
1624
1828
|
});
|
|
1625
|
-
return
|
|
1829
|
+
return created;
|
|
1626
1830
|
} catch (error) {
|
|
1627
|
-
|
|
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);
|
|
1628
1847
|
}
|
|
1629
1848
|
}
|
|
1630
1849
|
/**
|
|
1631
|
-
* createIssue 가 이슈를 만들지 않고 끝난 이유를
|
|
1850
|
+
* createIssue 가 이슈를 만들지 않고 끝난 이유를 남기고 onError 로 넘긴다.
|
|
1632
1851
|
*
|
|
1633
1852
|
* devLogger 는 프로덕션 빌드에서 침묵하는데, 이 실패는 예외로 드러나지 않으므로
|
|
1634
1853
|
* 통합한 쪽이 프로덕션에서도 알아야 한다. 접두어가 붙어 있어 우리 수집기가
|
|
1635
1854
|
* 자기 로그로 걸러내므로 고객 이슈의 콘솔 로그를 오염시키지 않는다.
|
|
1855
|
+
* onError 가 던져도 createIssue 는 던지지 않는다 — 전역 핸들러 안에서 부른 경우 루프가 된다.
|
|
1636
1856
|
*/
|
|
1637
|
-
skipCreateIssue(
|
|
1638
|
-
const args = [
|
|
1639
|
-
|
|
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);
|
|
1640
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
|
+
}
|
|
1641
1874
|
return null;
|
|
1642
1875
|
}
|
|
1643
1876
|
async loginAsMember() {
|
|
@@ -1682,8 +1915,35 @@ class HeadlessClientImpl {
|
|
|
1682
1915
|
this.logCollectors.stop();
|
|
1683
1916
|
this.autoIssueRateLimit.reset();
|
|
1684
1917
|
this.authBridge.destroy();
|
|
1918
|
+
this.maskingPolicy = this.createMaskingPolicy();
|
|
1919
|
+
this.maskingPolicyPrefetch = null;
|
|
1920
|
+
this.maskingPolicyFetchedAt = null;
|
|
1685
1921
|
this.state.reset();
|
|
1686
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
|
+
}
|
|
1687
1947
|
async initializeInternal() {
|
|
1688
1948
|
this.state.setSnapshot({ phase: "initializing", error: null });
|
|
1689
1949
|
try {
|
|
@@ -1706,6 +1966,7 @@ class HeadlessClientImpl {
|
|
|
1706
1966
|
filterSchema: { groups: [], unavailableReason: "public-mode" }
|
|
1707
1967
|
});
|
|
1708
1968
|
this.logCollectors.start();
|
|
1969
|
+
this.prefetchMaskingPolicy();
|
|
1709
1970
|
const { video, attachments } = this.uploadQueues;
|
|
1710
1971
|
void this.uploadQueues.resume([result.projectId]).then(() => {
|
|
1711
1972
|
void video.pruneExpired();
|
|
@@ -1723,25 +1984,62 @@ class HeadlessClientImpl {
|
|
|
1723
1984
|
const sessionReplay = this.sessionReplayController.getSnapshot();
|
|
1724
1985
|
if (!sessionReplay.enabled || !sessionReplay.canAttach) return null;
|
|
1725
1986
|
try {
|
|
1726
|
-
const snapshot = await this.sessionReplayController.takeSnapshot();
|
|
1987
|
+
const snapshot = await this.sessionReplayController.takeSnapshot([]);
|
|
1727
1988
|
return snapshot ? toSessionReplayMeta(snapshot) : null;
|
|
1728
1989
|
} catch {
|
|
1729
1990
|
return null;
|
|
1730
1991
|
}
|
|
1731
1992
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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
|
+
}
|
|
1736
2025
|
}
|
|
1737
|
-
/**
|
|
2026
|
+
/**
|
|
2027
|
+
* 스티커 목록에는 영상 URL 도 업로드를 포기한 까닭도 없다. 업로드가 끝날 때 받아 둔 값을 같은
|
|
2028
|
+
* 이슈에 남긴다. 까닭은 여전히 실패 상태일 때만 남긴다.
|
|
2029
|
+
*/
|
|
1738
2030
|
keepKnownVideos(issues) {
|
|
1739
|
-
const known = new Map(
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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;
|
|
2037
|
+
return {
|
|
2038
|
+
...issue,
|
|
2039
|
+
...video ? { video } : {},
|
|
2040
|
+
...reason ? { video_failure_reason: reason } : {}
|
|
2041
|
+
};
|
|
2042
|
+
});
|
|
1745
2043
|
}
|
|
1746
2044
|
/**
|
|
1747
2045
|
* 업로드가 끝나면 목록·상세의 같은 이슈에 영상 상태를 반영한다. 완료는 재조회해 영상 URL 까지
|
|
@@ -1752,12 +2050,18 @@ class HeadlessClientImpl {
|
|
|
1752
2050
|
const refreshed = event.type === "completed" ? await this.issueRepository.openIssue(event.issueId).catch(() => null) : null;
|
|
1753
2051
|
if (this.destroyed) return;
|
|
1754
2052
|
const videoStatus = event.type === "completed" ? "ready" : "failed";
|
|
2053
|
+
const failure = event.reason ? { video_failure_reason: event.reason } : {};
|
|
1755
2054
|
const { issues, activeIssue } = this.snapshot;
|
|
1756
2055
|
this.state.setSnapshot({
|
|
1757
2056
|
issues: issues.map(
|
|
1758
|
-
(issue) => issue.id === event.issueId ? {
|
|
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
|
|
1759
2063
|
),
|
|
1760
|
-
activeIssue: (activeIssue == null ? void 0 : activeIssue.id) === event.issueId ? refreshed ?? { ...activeIssue, video_status: videoStatus } : activeIssue
|
|
2064
|
+
activeIssue: (activeIssue == null ? void 0 : activeIssue.id) === event.issueId ? refreshed ?? { ...activeIssue, video_status: videoStatus, ...failure } : activeIssue
|
|
1761
2065
|
});
|
|
1762
2066
|
}
|
|
1763
2067
|
/** 리플레이가 연결되거나 포기되면 목록·상세의 같은 이슈에 상태를 반영한다. 소켓 전사는 상태가 없다. */
|
|
@@ -1840,15 +2144,16 @@ class HeadlessClientImpl {
|
|
|
1840
2144
|
this.beginBackgroundCapturePreparation(draft, target);
|
|
1841
2145
|
}
|
|
1842
2146
|
beginBackgroundCapturePreparation(draft, target) {
|
|
1843
|
-
var _a
|
|
2147
|
+
var _a;
|
|
1844
2148
|
const token = {};
|
|
1845
2149
|
this.captureSession = {
|
|
1846
2150
|
token,
|
|
2151
|
+
target,
|
|
1847
2152
|
screenshots: draft.mediaMode === "screenshot" ? (
|
|
1848
|
-
//
|
|
1849
|
-
//
|
|
1850
|
-
this.
|
|
1851
|
-
() =>
|
|
2153
|
+
// 영역은 초기화 때 받아 둔 정책 캐시의 선택자로 계산한다. 캐시가 없으면 빈 목록이고,
|
|
2154
|
+
// 미리보기를 칠할 때 덮개 검사에 걸려 한 번 다시 찍는다.
|
|
2155
|
+
loadCaptureSelectors(() => this.livePolicySource.getMaskingPolicySnapshot()).then(
|
|
2156
|
+
(selectors) => this.captureScreenshots(target, selectors ?? [])
|
|
1852
2157
|
)
|
|
1853
2158
|
) : null,
|
|
1854
2159
|
formSchema: this.loadFormSchema({})
|
|
@@ -1857,11 +2162,10 @@ class HeadlessClientImpl {
|
|
|
1857
2162
|
var _a2;
|
|
1858
2163
|
return ((_a2 = this.captureSession) == null ? void 0 : _a2.token) === token;
|
|
1859
2164
|
};
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
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) => {
|
|
1865
2169
|
if (!isCurrentSession()) return;
|
|
1866
2170
|
this.state.setSnapshot({ formSchema });
|
|
1867
2171
|
}));
|
|
@@ -1874,6 +2178,64 @@ class HeadlessClientImpl {
|
|
|
1874
2178
|
});
|
|
1875
2179
|
}
|
|
1876
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
|
+
}
|
|
1877
2239
|
mergeDraft(patch) {
|
|
1878
2240
|
const draft = this.snapshot.draft;
|
|
1879
2241
|
if (!draft) return;
|