issue-sticker 0.8.0 → 0.9.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 +27 -0
- package/README.md +33 -0
- package/dist/{externalUserPayload-k9LSBeMB.js → externalUserPayload-CVp5Hz2L.js} +1027 -601
- package/dist/issue-sticker-headless-react.js +158 -22
- package/dist/issue-sticker-headless.js +43 -10
- package/dist/issue-sticker-react.js +442 -109
- package/dist/{mouse-pointer-2-BR00y3ir.js → three-B7nbYqGK.js} +855 -12
- package/dist/{tooltip-DH02R32-.js → tooltip-BBukUPo_.js} +1 -1
- package/headless-react.d.ts +26 -0
- package/headless.d.ts +28 -0
- package/package.json +6 -2
- package/react.d.ts +28 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { q as gzip_1,
|
|
1
|
+
import { q as onThreeRendererRegistered, t as shouldOverlayCanvas, v as getThreeHandle, C as CANVAS_INTERACTION_TAG, w as rendersBlank, x as gzip_1, y as redactUrlSecrets, z as redactSensitiveValues, A as record, B as resolvePrivacyConfig, E as isElementFullyMasked, F as grabCanvasFrames, G as applyScreenshotPrivacyMask, H as overlayCanvasFrames, I as forEachElementDeep, J as redactSensitiveText, K as REDACTED_BODY_VALUE, L as isSensitiveFieldName, N as redactSensitiveHeaders, Q as redactSensitiveBody, a as cn, o as useCallbackRef$1, h as useComposedRefs, e as Primitive, P as Presence, i as Portal$1, f as composeEventHandlers, j as createContextScope, k as createSlot, D as DismissableLayer, S as Content, T as createPopperScope, l as useControllableState, U as Root2$1, m as useId, V as Anchor, W as Arrow, b as usePortalContainer, d as OVERLAY_LAYER, s as stopEscapePropagation, n as useLayoutEffect2, X as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, Y as clsx, Z as Slot$2, O as OVERLAY_LAYER_INDEX, M as MousePointer2 } from "./three-B7nbYqGK.js";
|
|
2
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import React__default, { useState, createContext, useContext, useCallback, useRef, useLayoutEffect, useEffect, useMemo } from "react";
|
|
@@ -18,10 +18,12 @@ function getDefaultDisplayMedia() {
|
|
|
18
18
|
if (typeof navigator === "undefined" || !((_a = navigator.mediaDevices) == null ? void 0 : _a.getDisplayMedia)) {
|
|
19
19
|
throw new Error("Screen recording is not supported in this browser.");
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
video: { frameRate: 30 },
|
|
23
|
-
audio: false
|
|
24
|
-
|
|
21
|
+
const options = {
|
|
22
|
+
video: { frameRate: 30, displaySurface: "browser" },
|
|
23
|
+
audio: false,
|
|
24
|
+
selfBrowserSurface: "include"
|
|
25
|
+
};
|
|
26
|
+
return navigator.mediaDevices.getDisplayMedia(options);
|
|
25
27
|
}
|
|
26
28
|
function createDefaultMediaRecorder(stream, options) {
|
|
27
29
|
if (typeof MediaRecorder === "undefined") {
|
|
@@ -205,98 +207,361 @@ class ScreenRecordingController {
|
|
|
205
207
|
this.listeners.forEach((listener) => listener(this.snapshot));
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
".
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"[data-cs-mask]",
|
|
251
|
-
"[data-cs-encrypt]",
|
|
252
|
-
// Heap
|
|
253
|
-
"[data-heap-redact-text]",
|
|
254
|
-
"[data-heap-redact-attributes]",
|
|
255
|
-
// Amplitude
|
|
256
|
-
"[data-amp-mask]",
|
|
257
|
-
// Matomo
|
|
258
|
-
"[data-matomo-mask]"
|
|
259
|
-
];
|
|
260
|
-
const FOREIGN_UNMASK_SELECTORS = [
|
|
261
|
-
".fs-unmask",
|
|
262
|
-
".sentry-unmask",
|
|
263
|
-
"[data-clarity-unmask]",
|
|
264
|
-
"[data-amp-unmask]",
|
|
265
|
-
"[data-cs-capture]"
|
|
266
|
-
];
|
|
267
|
-
const SESSION_REPLAY_FULL_MASK_SELECTOR = [
|
|
268
|
-
`.${SESSION_REPLAY_MASK_CLASS}`,
|
|
269
|
-
...FOREIGN_MASK_SELECTORS
|
|
270
|
-
].join(",");
|
|
271
|
-
const SESSION_REPLAY_BLOCK_SELECTOR = [
|
|
272
|
-
`.${SESSION_REPLAY_BLOCK_CLASS}`,
|
|
273
|
-
...FOREIGN_BLOCK_SELECTORS
|
|
274
|
-
].join(",");
|
|
275
|
-
const SESSION_REPLAY_UNMASK_SELECTOR = [
|
|
276
|
-
`.${SESSION_REPLAY_UNMASK_CLASS}`,
|
|
277
|
-
...FOREIGN_UNMASK_SELECTORS
|
|
278
|
-
].join(",");
|
|
279
|
-
function isElementFullyMasked(element, maskSelector = SESSION_REPLAY_FULL_MASK_SELECTOR, unmaskSelector = SESSION_REPLAY_UNMASK_SELECTOR) {
|
|
280
|
-
if (!element) return false;
|
|
210
|
+
const SNAPSHOT_MAX_WIDTH = 640;
|
|
211
|
+
const SNAPSHOT_QUALITY = 0.8;
|
|
212
|
+
const SAMPLE_INTERVAL_MS = 250;
|
|
213
|
+
const SAMPLED_MAX_WIDTH = 480;
|
|
214
|
+
const SAMPLED_QUALITY = 0.6;
|
|
215
|
+
const DRAG_THRESHOLD_PX = 6;
|
|
216
|
+
const DISCRETE_MIN_GAP_MS = 60;
|
|
217
|
+
const SETTLE_TAIL_MS = 300;
|
|
218
|
+
function isThreeNamespace(value) {
|
|
219
|
+
const candidate = value;
|
|
220
|
+
return typeof (candidate == null ? void 0 : candidate.Raycaster) === "function" && typeof (candidate == null ? void 0 : candidate.Vector2) === "function";
|
|
221
|
+
}
|
|
222
|
+
function isHelper(object) {
|
|
223
|
+
let current = object;
|
|
224
|
+
while (current) {
|
|
225
|
+
if (typeof current.type === "string" && current.type.endsWith("Helper")) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
current = current.parent;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function nameOf(object) {
|
|
233
|
+
let current = object;
|
|
234
|
+
while (current) {
|
|
235
|
+
if (current.name) {
|
|
236
|
+
return current.name;
|
|
237
|
+
}
|
|
238
|
+
current = current.parent;
|
|
239
|
+
}
|
|
240
|
+
return (object == null ? void 0 : object.type) ?? null;
|
|
241
|
+
}
|
|
242
|
+
function pickTarget(handle, pointer) {
|
|
243
|
+
var _a;
|
|
244
|
+
const three = handle.three;
|
|
245
|
+
if (!isThreeNamespace(three)) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const children = (_a = handle.scene) == null ? void 0 : _a.children;
|
|
249
|
+
if (!Array.isArray(children)) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
281
252
|
try {
|
|
282
|
-
const
|
|
283
|
-
|
|
253
|
+
const raycaster = new three.Raycaster();
|
|
254
|
+
raycaster.setFromCamera(new three.Vector2(pointer[0], pointer[1]), handle.camera);
|
|
255
|
+
const hits = raycaster.intersectObjects(children, true);
|
|
256
|
+
const hit = hits.find(
|
|
257
|
+
(candidate) => {
|
|
258
|
+
var _a2, _b;
|
|
259
|
+
return ((_a2 = candidate.object) == null ? void 0 : _a2.isMesh) === true && ((_b = candidate.object) == null ? void 0 : _b.visible) !== false && !isHelper(candidate.object);
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
return hit ? nameOf(hit.object) : null;
|
|
284
263
|
} catch {
|
|
285
|
-
return
|
|
264
|
+
return null;
|
|
286
265
|
}
|
|
287
266
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
267
|
+
function cameraMatrixOf(handle) {
|
|
268
|
+
var _a, _b;
|
|
269
|
+
const matrix = (_a = handle.camera) == null ? void 0 : _a.matrixWorld;
|
|
270
|
+
try {
|
|
271
|
+
const values = (_b = matrix == null ? void 0 : matrix.toArray) == null ? void 0 : _b.call(matrix);
|
|
272
|
+
return Array.isArray(values) ? values : null;
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const FINGERPRINT_WIDTH = 32;
|
|
278
|
+
const FINGERPRINT_HEIGHT = 18;
|
|
279
|
+
async function captureSnapshot(canvas, handle, maxWidth, quality) {
|
|
280
|
+
try {
|
|
281
|
+
if (handle) {
|
|
282
|
+
handle.renderer.render(handle.scene, handle.camera);
|
|
283
|
+
} else if (rendersBlank(canvas)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const bitmap = await createImageBitmap(canvas);
|
|
287
|
+
try {
|
|
288
|
+
const scale = Math.min(1, maxWidth / bitmap.width);
|
|
289
|
+
const output = document.createElement("canvas");
|
|
290
|
+
output.width = Math.max(1, Math.round(bitmap.width * scale));
|
|
291
|
+
output.height = Math.max(1, Math.round(bitmap.height * scale));
|
|
292
|
+
const context = output.getContext("2d");
|
|
293
|
+
if (!context) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
context.drawImage(bitmap, 0, 0, output.width, output.height);
|
|
297
|
+
const thumb = document.createElement("canvas");
|
|
298
|
+
thumb.width = FINGERPRINT_WIDTH;
|
|
299
|
+
thumb.height = FINGERPRINT_HEIGHT;
|
|
300
|
+
const thumbContext = thumb.getContext("2d");
|
|
301
|
+
thumbContext == null ? void 0 : thumbContext.drawImage(bitmap, 0, 0, FINGERPRINT_WIDTH, FINGERPRINT_HEIGHT);
|
|
302
|
+
const fingerprint = thumbContext ? Array.from(
|
|
303
|
+
thumbContext.getImageData(0, 0, FINGERPRINT_WIDTH, FINGERPRINT_HEIGHT).data
|
|
304
|
+
).join(",") : "";
|
|
305
|
+
return { dataUrl: output.toDataURL("image/webp", quality), fingerprint };
|
|
306
|
+
} finally {
|
|
307
|
+
bitmap.close();
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const FRAME_WAIT_MS = 120;
|
|
314
|
+
function settleFrame() {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
let done = false;
|
|
317
|
+
const finish = () => {
|
|
318
|
+
if (!done) {
|
|
319
|
+
done = true;
|
|
320
|
+
resolve();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
requestAnimationFrame(finish);
|
|
324
|
+
setTimeout(finish, FRAME_WAIT_MS);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function resolveNodeId(canvas) {
|
|
328
|
+
var _a;
|
|
329
|
+
try {
|
|
330
|
+
const { record: record2 } = await import("./three-B7nbYqGK.js").then((n) => n.a0);
|
|
331
|
+
const id = (_a = record2.mirror) == null ? void 0 : _a.getId(canvas);
|
|
332
|
+
return typeof id === "number" && id > 0 ? id : null;
|
|
333
|
+
} catch {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function canvasIndexOf(canvas) {
|
|
338
|
+
return Array.prototype.indexOf.call(document.querySelectorAll("canvas"), canvas);
|
|
339
|
+
}
|
|
340
|
+
function startCanvasInteractionTracking(options = {}) {
|
|
341
|
+
if (typeof document === "undefined") {
|
|
342
|
+
return Object.assign(() => void 0, { captureBaseline: () => void 0 });
|
|
343
|
+
}
|
|
344
|
+
const snapshotMaxWidth = options.snapshotMaxWidth ?? SNAPSHOT_MAX_WIDTH;
|
|
345
|
+
const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
|
|
346
|
+
const lastSnapshot = /* @__PURE__ */ new WeakMap();
|
|
347
|
+
let pending = null;
|
|
348
|
+
const emit2 = options.emit ?? ((tag, payload) => {
|
|
349
|
+
void import("./three-B7nbYqGK.js").then((n) => n.a0).then(({ addCustomEvent }) => addCustomEvent(tag, payload));
|
|
350
|
+
});
|
|
351
|
+
const pointerOf = (started, rect) => [
|
|
352
|
+
(started.x - rect.left) / rect.width * 2 - 1,
|
|
353
|
+
-((started.y - rect.top) / rect.height) * 2 + 1
|
|
354
|
+
];
|
|
355
|
+
const trackedCanvases = () => Array.from(document.querySelectorAll("canvas")).filter((canvas) => {
|
|
356
|
+
const rect = canvas.getBoundingClientRect();
|
|
357
|
+
return rect.width >= 1 && rect.height >= 1 && shouldOverlayCanvas(canvas, []);
|
|
358
|
+
});
|
|
359
|
+
const emitShot = async (canvas, shot) => {
|
|
360
|
+
const handle = getThreeHandle(canvas);
|
|
361
|
+
const snapshot = await captureSnapshot(
|
|
362
|
+
canvas,
|
|
363
|
+
handle,
|
|
364
|
+
shot.detail ? snapshotMaxWidth : SAMPLED_MAX_WIDTH,
|
|
365
|
+
shot.detail ? SNAPSHOT_QUALITY : SAMPLED_QUALITY
|
|
366
|
+
);
|
|
367
|
+
if (snapshot && shot.sampled && !shot.baseline && lastSnapshot.get(canvas) === snapshot.fingerprint) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (snapshot) {
|
|
371
|
+
lastSnapshot.set(canvas, snapshot.fingerprint);
|
|
372
|
+
}
|
|
373
|
+
const cameraMatrix = handle ? cameraMatrixOf(handle) : null;
|
|
374
|
+
if (!snapshot && !shot.target && !cameraMatrix) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const payload = {
|
|
378
|
+
kind: shot.kind,
|
|
379
|
+
target: shot.target,
|
|
380
|
+
nodeId: await resolveNodeId(canvas),
|
|
381
|
+
canvasIndex: canvasIndexOf(canvas),
|
|
382
|
+
pointer: shot.pointer,
|
|
383
|
+
cameraStartMatrix: shot.cameraStartMatrix,
|
|
384
|
+
cameraMatrix,
|
|
385
|
+
snapshot: (snapshot == null ? void 0 : snapshot.dataUrl) ?? null,
|
|
386
|
+
...shot.sampled ? { sampled: true } : {},
|
|
387
|
+
...shot.baseline ? { baseline: true } : {}
|
|
388
|
+
};
|
|
389
|
+
try {
|
|
390
|
+
emit2(CANVAS_INTERACTION_TAG, payload);
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
const sweep = async (shot) => {
|
|
395
|
+
for (const canvas of trackedCanvases()) {
|
|
396
|
+
await emitShot(canvas, {
|
|
397
|
+
kind: "external",
|
|
398
|
+
target: null,
|
|
399
|
+
pointer: null,
|
|
400
|
+
cameraStartMatrix: null,
|
|
401
|
+
...shot
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
let sweeping = false;
|
|
406
|
+
let pendingSweep = null;
|
|
407
|
+
const runSweep = async (shot) => {
|
|
408
|
+
if (sweeping) {
|
|
409
|
+
pendingSweep = shot;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
sweeping = true;
|
|
413
|
+
try {
|
|
414
|
+
await sweep(shot);
|
|
415
|
+
while (pendingSweep) {
|
|
416
|
+
const next = pendingSweep;
|
|
417
|
+
pendingSweep = null;
|
|
418
|
+
await sweep(next);
|
|
419
|
+
}
|
|
420
|
+
} finally {
|
|
421
|
+
sweeping = false;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
let sampleTimer = null;
|
|
425
|
+
let tailTimer = null;
|
|
426
|
+
let lastDiscreteAt = 0;
|
|
427
|
+
const clearTimers = () => {
|
|
428
|
+
if (sampleTimer !== null) {
|
|
429
|
+
window.clearInterval(sampleTimer);
|
|
430
|
+
sampleTimer = null;
|
|
431
|
+
}
|
|
432
|
+
if (tailTimer !== null) {
|
|
433
|
+
window.clearTimeout(tailTimer);
|
|
434
|
+
tailTimer = null;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
const markChanged = () => {
|
|
438
|
+
if (sampleTimer === null && sampleIntervalMs > 0) {
|
|
439
|
+
sampleTimer = window.setInterval(() => {
|
|
440
|
+
void runSweep({ sampled: true, detail: false });
|
|
441
|
+
}, sampleIntervalMs);
|
|
442
|
+
}
|
|
443
|
+
if (tailTimer !== null) {
|
|
444
|
+
window.clearTimeout(tailTimer);
|
|
445
|
+
}
|
|
446
|
+
tailTimer = window.setTimeout(() => {
|
|
447
|
+
tailTimer = null;
|
|
448
|
+
if (sampleTimer !== null) {
|
|
449
|
+
window.clearInterval(sampleTimer);
|
|
450
|
+
sampleTimer = null;
|
|
451
|
+
}
|
|
452
|
+
void (async () => {
|
|
453
|
+
await settleFrame();
|
|
454
|
+
await runSweep({ sampled: true, detail: true });
|
|
455
|
+
})();
|
|
456
|
+
}, SETTLE_TAIL_MS);
|
|
457
|
+
};
|
|
458
|
+
const onPointerDown = (event) => {
|
|
459
|
+
const target = event.target;
|
|
460
|
+
if (!(target instanceof HTMLCanvasElement) || !shouldOverlayCanvas(target, [])) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const handle = getThreeHandle(target);
|
|
464
|
+
pending = {
|
|
465
|
+
canvas: target,
|
|
466
|
+
x: event.clientX,
|
|
467
|
+
y: event.clientY,
|
|
468
|
+
cameraStartMatrix: handle ? cameraMatrixOf(handle) : null
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
const onPointerMove = (event) => {
|
|
472
|
+
const started = pending;
|
|
473
|
+
if (!started) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (Math.hypot(event.clientX - started.x, event.clientY - started.y) <= DRAG_THRESHOLD_PX) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
markChanged();
|
|
480
|
+
};
|
|
481
|
+
const captureDiscrete = () => {
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
if (now - lastDiscreteAt >= DISCRETE_MIN_GAP_MS) {
|
|
484
|
+
lastDiscreteAt = now;
|
|
485
|
+
void (async () => {
|
|
486
|
+
await settleFrame();
|
|
487
|
+
await runSweep({ sampled: true, detail: false });
|
|
488
|
+
})();
|
|
489
|
+
}
|
|
490
|
+
markChanged();
|
|
491
|
+
};
|
|
492
|
+
const onContinuousInput = () => {
|
|
493
|
+
markChanged();
|
|
494
|
+
};
|
|
495
|
+
const onPointerUp = (event) => {
|
|
496
|
+
const started = pending;
|
|
497
|
+
pending = null;
|
|
498
|
+
if (!started) {
|
|
499
|
+
captureDiscrete();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
clearTimers();
|
|
503
|
+
const handle = getThreeHandle(started.canvas);
|
|
504
|
+
const rect = started.canvas.getBoundingClientRect();
|
|
505
|
+
if (rect.width < 1 || rect.height < 1) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const moved = Math.hypot(event.clientX - started.x, event.clientY - started.y);
|
|
509
|
+
const kind = moved > DRAG_THRESHOLD_PX ? "drag" : "click";
|
|
510
|
+
const target = handle ? pickTarget(handle, pointerOf(started, rect)) : null;
|
|
511
|
+
void (async () => {
|
|
512
|
+
await settleFrame();
|
|
513
|
+
await emitShot(started.canvas, {
|
|
514
|
+
kind,
|
|
515
|
+
target,
|
|
516
|
+
pointer: pointerOf(started, rect),
|
|
517
|
+
cameraStartMatrix: started.cameraStartMatrix,
|
|
518
|
+
sampled: false,
|
|
519
|
+
detail: true
|
|
520
|
+
});
|
|
521
|
+
})();
|
|
522
|
+
markChanged();
|
|
523
|
+
};
|
|
524
|
+
const captureBaseline = () => {
|
|
525
|
+
void (async () => {
|
|
526
|
+
await settleFrame();
|
|
527
|
+
await runSweep({ sampled: true, detail: true, baseline: true });
|
|
528
|
+
})();
|
|
529
|
+
};
|
|
530
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
531
|
+
document.addEventListener("pointermove", onPointerMove, true);
|
|
532
|
+
document.addEventListener("pointerup", onPointerUp, true);
|
|
533
|
+
document.addEventListener("pointercancel", onPointerUp, true);
|
|
534
|
+
document.addEventListener("input", onContinuousInput, true);
|
|
535
|
+
document.addEventListener("keydown", onContinuousInput, true);
|
|
536
|
+
document.addEventListener("change", captureDiscrete, true);
|
|
537
|
+
const unsubscribeRegistration = onThreeRendererRegistered((canvas) => {
|
|
538
|
+
void (async () => {
|
|
539
|
+
await settleFrame();
|
|
540
|
+
await emitShot(canvas, {
|
|
541
|
+
kind: "external",
|
|
542
|
+
target: null,
|
|
543
|
+
pointer: null,
|
|
544
|
+
cameraStartMatrix: null,
|
|
545
|
+
sampled: true,
|
|
546
|
+
detail: true,
|
|
547
|
+
baseline: true
|
|
548
|
+
});
|
|
549
|
+
})();
|
|
550
|
+
});
|
|
551
|
+
const stop = () => {
|
|
552
|
+
clearTimers();
|
|
553
|
+
unsubscribeRegistration();
|
|
554
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
555
|
+
document.removeEventListener("pointermove", onPointerMove, true);
|
|
556
|
+
document.removeEventListener("pointerup", onPointerUp, true);
|
|
557
|
+
document.removeEventListener("pointercancel", onPointerUp, true);
|
|
558
|
+
document.removeEventListener("input", onContinuousInput, true);
|
|
559
|
+
document.removeEventListener("keydown", onContinuousInput, true);
|
|
560
|
+
document.removeEventListener("change", captureDiscrete, true);
|
|
561
|
+
pending = null;
|
|
562
|
+
};
|
|
563
|
+
captureBaseline();
|
|
564
|
+
return Object.assign(stop, { captureBaseline });
|
|
300
565
|
}
|
|
301
566
|
function createSegments() {
|
|
302
567
|
return [{ events: [] }];
|
|
@@ -329,264 +594,49 @@ function trimSegments(segments, windowMs, now, checkoutMs) {
|
|
|
329
594
|
}
|
|
330
595
|
return segments.slice(startIdx);
|
|
331
596
|
}
|
|
597
|
+
function spanSeconds(segments) {
|
|
598
|
+
var _a, _b;
|
|
599
|
+
const events = flattenEvents(segments);
|
|
600
|
+
const first = (_a = events[0]) == null ? void 0 : _a.timestamp;
|
|
601
|
+
const last = (_b = events[events.length - 1]) == null ? void 0 : _b.timestamp;
|
|
602
|
+
if (first === void 0 || last === void 0) return 0;
|
|
603
|
+
return Math.round((last - first) / 1e3);
|
|
604
|
+
}
|
|
332
605
|
function flattenEvents(segments) {
|
|
333
606
|
return segments.flatMap((s2) => s2.events);
|
|
334
607
|
}
|
|
335
608
|
function dropEventsAfter(segments, cutoff) {
|
|
336
609
|
return segments.map((s2) => ({ events: s2.events.filter((e2) => e2.timestamp <= cutoff) })).filter((s2) => s2.events.length > 0);
|
|
337
610
|
}
|
|
338
|
-
|
|
611
|
+
const REPLAY_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
612
|
+
async function sha256Hex$1(bytes) {
|
|
339
613
|
const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
|
|
340
614
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
341
615
|
}
|
|
342
616
|
async function serializeSnapshot(input) {
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
|
|
617
|
+
const redact = input.redactEvents ?? ((e2) => e2);
|
|
618
|
+
const maxBytes = input.maxBytes ?? REPLAY_SNAPSHOT_MAX_BYTES;
|
|
619
|
+
const compress2 = (segments2) => {
|
|
620
|
+
const events2 = redact(flattenEvents(segments2));
|
|
621
|
+
return { events: events2, payload: gzip_1(JSON.stringify(events2)) };
|
|
622
|
+
};
|
|
623
|
+
let segments = input.segments;
|
|
624
|
+
let { events, payload } = compress2(segments);
|
|
625
|
+
while (payload.byteLength > maxBytes && segments.length > 1) {
|
|
626
|
+
segments = segments.slice(1);
|
|
627
|
+
({ events, payload } = compress2(segments));
|
|
628
|
+
}
|
|
629
|
+
const checksumSha256 = await sha256Hex$1(payload);
|
|
347
630
|
return {
|
|
348
631
|
payload,
|
|
349
632
|
checksumSha256,
|
|
350
|
-
windowSeconds:
|
|
633
|
+
windowSeconds: spanSeconds(segments),
|
|
351
634
|
eventCount: events.length,
|
|
352
635
|
sizeBytes: payload.byteLength,
|
|
353
636
|
rrwebVersion: input.rrwebVersion,
|
|
354
637
|
privacyConfigVersion: input.privacyConfigVersion
|
|
355
638
|
};
|
|
356
639
|
}
|
|
357
|
-
const REDACTED_HEADER_VALUE = "[redacted]";
|
|
358
|
-
const SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
359
|
-
"authorization",
|
|
360
|
-
"proxy-authorization",
|
|
361
|
-
"cookie",
|
|
362
|
-
"set-cookie",
|
|
363
|
-
"x-api-key",
|
|
364
|
-
"api-key",
|
|
365
|
-
"apikey",
|
|
366
|
-
"x-auth-token",
|
|
367
|
-
"x-access-token",
|
|
368
|
-
"x-refresh-token",
|
|
369
|
-
"x-session-token",
|
|
370
|
-
"x-csrf-token",
|
|
371
|
-
"x-xsrf-token",
|
|
372
|
-
"x-amz-security-token",
|
|
373
|
-
"x-goog-api-key"
|
|
374
|
-
]);
|
|
375
|
-
function isSensitiveHeaderName(headerName) {
|
|
376
|
-
return SENSITIVE_HEADER_NAMES.has(headerName.toLowerCase());
|
|
377
|
-
}
|
|
378
|
-
function redactSensitiveHeaders(headers) {
|
|
379
|
-
if (!headers) return headers;
|
|
380
|
-
let changed = false;
|
|
381
|
-
const redacted = {};
|
|
382
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
383
|
-
if (value && isSensitiveHeaderName(name)) {
|
|
384
|
-
redacted[name] = REDACTED_HEADER_VALUE;
|
|
385
|
-
changed = true;
|
|
386
|
-
} else {
|
|
387
|
-
redacted[name] = value;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return changed ? redacted : headers;
|
|
391
|
-
}
|
|
392
|
-
const REDACTED_BODY_VALUE = "[redacted]";
|
|
393
|
-
const SENSITIVE_KEY_MARKERS = [
|
|
394
|
-
"password",
|
|
395
|
-
"passwd",
|
|
396
|
-
"pwd",
|
|
397
|
-
"secret",
|
|
398
|
-
"token",
|
|
399
|
-
"apikey",
|
|
400
|
-
"privatekey",
|
|
401
|
-
"credential",
|
|
402
|
-
// 결제·본인확인 계열. 토스페이먼츠 공식 문서의 필드명을 그대로 딴다.
|
|
403
|
-
// cardNumber / customerIdentityNumber / businessNumber / accountNumber(가상계좌)
|
|
404
|
-
"cardnumber",
|
|
405
|
-
"identitynumber",
|
|
406
|
-
"businessnumber",
|
|
407
|
-
"registrationnumber",
|
|
408
|
-
"accountnumber"
|
|
409
|
-
];
|
|
410
|
-
const SENSITIVE_KEY_EXACT = /* @__PURE__ */ new Set([
|
|
411
|
-
"otp",
|
|
412
|
-
"auth",
|
|
413
|
-
"authorization",
|
|
414
|
-
"bearer",
|
|
415
|
-
// 토스페이먼츠. billingKey + customerKey 조합이면 결제를 낼 수 있고, authKey 는 일회성
|
|
416
|
-
// 인증 키다(우리 admin 결제 성공 URL 의 쿼리 파라미터로 실제로 실린다).
|
|
417
|
-
// paymentKey 는 넣지 않는다 — 시크릿 키 없이는 아무것도 못 하는 식별자이고, 결제 장애
|
|
418
|
-
// 진단의 출발점이라 가리면 로그가 쓸모를 잃는다.
|
|
419
|
-
"billingkey",
|
|
420
|
-
"customerkey",
|
|
421
|
-
"authkey"
|
|
422
|
-
]);
|
|
423
|
-
const MAX_WALK_DEPTH = 20;
|
|
424
|
-
function isSensitiveFieldName(name) {
|
|
425
|
-
const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
|
|
426
|
-
if (SENSITIVE_KEY_EXACT.has(normalized)) {
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
return SENSITIVE_KEY_MARKERS.some((marker) => normalized.includes(marker));
|
|
430
|
-
}
|
|
431
|
-
function redactJsonValue(value, depth) {
|
|
432
|
-
if (typeof value === "string") {
|
|
433
|
-
const redacted = redactSensitiveValues(value);
|
|
434
|
-
return { value: redacted, changed: redacted !== value };
|
|
435
|
-
}
|
|
436
|
-
if (depth > MAX_WALK_DEPTH || value === null || typeof value !== "object") {
|
|
437
|
-
return { value, changed: false };
|
|
438
|
-
}
|
|
439
|
-
if (Array.isArray(value)) {
|
|
440
|
-
let changed2 = false;
|
|
441
|
-
const next2 = value.map((item) => {
|
|
442
|
-
const result = redactJsonValue(item, depth + 1);
|
|
443
|
-
changed2 || (changed2 = result.changed);
|
|
444
|
-
return result.value;
|
|
445
|
-
});
|
|
446
|
-
return changed2 ? { value: next2, changed: changed2 } : { value, changed: changed2 };
|
|
447
|
-
}
|
|
448
|
-
let changed = false;
|
|
449
|
-
const next = {};
|
|
450
|
-
for (const [key, item] of Object.entries(value)) {
|
|
451
|
-
if (item !== "" && item != null && typeof item !== "boolean" && isSensitiveFieldName(key)) {
|
|
452
|
-
next[key] = REDACTED_BODY_VALUE;
|
|
453
|
-
changed = true;
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
const result = redactJsonValue(item, depth + 1);
|
|
457
|
-
changed || (changed = result.changed);
|
|
458
|
-
next[key] = result.value;
|
|
459
|
-
}
|
|
460
|
-
return changed ? { value: next, changed } : { value, changed };
|
|
461
|
-
}
|
|
462
|
-
function redactKeyValuePairs(body) {
|
|
463
|
-
let changed = false;
|
|
464
|
-
const redacted = body.split("&").map((pair) => {
|
|
465
|
-
const separator = pair.indexOf("=");
|
|
466
|
-
if (separator <= 0) {
|
|
467
|
-
return pair;
|
|
468
|
-
}
|
|
469
|
-
const rawKey = pair.slice(0, separator);
|
|
470
|
-
const rawValue = pair.slice(separator + 1);
|
|
471
|
-
if (!rawValue) {
|
|
472
|
-
return pair;
|
|
473
|
-
}
|
|
474
|
-
if (isSensitiveFieldName(decodeURIComponent(rawKey))) {
|
|
475
|
-
changed = true;
|
|
476
|
-
return `${rawKey}=${encodeURIComponent(REDACTED_BODY_VALUE)}`;
|
|
477
|
-
}
|
|
478
|
-
const redactedValue = redactSensitiveValues(decodeURIComponent(rawValue));
|
|
479
|
-
if (redactedValue === decodeURIComponent(rawValue)) {
|
|
480
|
-
return pair;
|
|
481
|
-
}
|
|
482
|
-
changed = true;
|
|
483
|
-
return `${rawKey}=${encodeURIComponent(redactedValue)}`;
|
|
484
|
-
}).join("&");
|
|
485
|
-
return changed ? redacted : body;
|
|
486
|
-
}
|
|
487
|
-
function looksLikeFormUrlEncoded(body) {
|
|
488
|
-
return /^[^=&\s]+=[^&]*(&[^=&\s]+=[^&]*)*$/.test(body);
|
|
489
|
-
}
|
|
490
|
-
function redactSensitiveBody(body) {
|
|
491
|
-
if (!body) return body;
|
|
492
|
-
const trimmed = body.trim();
|
|
493
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
494
|
-
try {
|
|
495
|
-
const parsed = JSON.parse(trimmed);
|
|
496
|
-
const { value, changed } = redactJsonValue(parsed, 0);
|
|
497
|
-
return changed ? JSON.stringify(value) : body;
|
|
498
|
-
} catch {
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
if (looksLikeFormUrlEncoded(trimmed)) {
|
|
502
|
-
const redacted = redactKeyValuePairs(trimmed);
|
|
503
|
-
return redacted === trimmed ? body : redacted;
|
|
504
|
-
}
|
|
505
|
-
return body;
|
|
506
|
-
}
|
|
507
|
-
const JWT_PATTERN = /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g;
|
|
508
|
-
const BEARER_PATTERN = /(Bearer\s+)[A-Za-z0-9._~+/-]{12,}=*/gi;
|
|
509
|
-
const CARD_CANDIDATE_PATTERN = /\b[2-6](?:[ -]?\d){13,18}\b/g;
|
|
510
|
-
const RRN_CANDIDATE_PATTERN = /\b(\d{2})(\d{2})(\d{2})-?([1-8])(\d{6})\b/g;
|
|
511
|
-
const BRN_CANDIDATE_PATTERN = /\b(\d{3})-?(\d{2})-?(\d{5})\b/g;
|
|
512
|
-
const BRN_WEIGHTS = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
|
513
|
-
const KR_MOBILE_PATTERN = /\b01[016789]-?\d{3,4}-?\d{4}\b/g;
|
|
514
|
-
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g;
|
|
515
|
-
function redactEmails(text) {
|
|
516
|
-
if (!text) return text;
|
|
517
|
-
const out = text.replace(EMAIL_PATTERN, REDACTED_BODY_VALUE);
|
|
518
|
-
return out === text ? text : out;
|
|
519
|
-
}
|
|
520
|
-
function passesBrnChecksum(digits) {
|
|
521
|
-
let sum = 0;
|
|
522
|
-
for (let i2 = 0; i2 < 9; i2 += 1) {
|
|
523
|
-
sum += (digits.charCodeAt(i2) - 48) * BRN_WEIGHTS[i2];
|
|
524
|
-
}
|
|
525
|
-
sum += Math.floor((digits.charCodeAt(8) - 48) * 5 / 10);
|
|
526
|
-
return (10 - sum % 10) % 10 === digits.charCodeAt(9) - 48;
|
|
527
|
-
}
|
|
528
|
-
function passesLuhn(digits) {
|
|
529
|
-
let sum = 0;
|
|
530
|
-
let double = false;
|
|
531
|
-
for (let i2 = digits.length - 1; i2 >= 0; i2 -= 1) {
|
|
532
|
-
let digit = digits.charCodeAt(i2) - 48;
|
|
533
|
-
if (double) {
|
|
534
|
-
digit *= 2;
|
|
535
|
-
if (digit > 9) digit -= 9;
|
|
536
|
-
}
|
|
537
|
-
sum += digit;
|
|
538
|
-
double = !double;
|
|
539
|
-
}
|
|
540
|
-
return sum % 10 === 0;
|
|
541
|
-
}
|
|
542
|
-
function looksLikeBirthDate(month, day) {
|
|
543
|
-
const monthValue = Number(month);
|
|
544
|
-
const dayValue = Number(day);
|
|
545
|
-
return monthValue >= 1 && monthValue <= 12 && dayValue >= 1 && dayValue <= 31;
|
|
546
|
-
}
|
|
547
|
-
function redactSensitiveValues(text) {
|
|
548
|
-
if (!text) return text;
|
|
549
|
-
let out = text;
|
|
550
|
-
out = out.replace(JWT_PATTERN, REDACTED_BODY_VALUE);
|
|
551
|
-
out = out.replace(BEARER_PATTERN, `$1${REDACTED_BODY_VALUE}`);
|
|
552
|
-
out = out.replace(CARD_CANDIDATE_PATTERN, (match2) => {
|
|
553
|
-
const digits = match2.replace(/[ -]/g, "");
|
|
554
|
-
return digits.length >= 14 && digits.length <= 19 && passesLuhn(digits) ? REDACTED_BODY_VALUE : match2;
|
|
555
|
-
});
|
|
556
|
-
out = out.replace(
|
|
557
|
-
RRN_CANDIDATE_PATTERN,
|
|
558
|
-
(match2, _year, month, day) => looksLikeBirthDate(month, day) ? REDACTED_BODY_VALUE : match2
|
|
559
|
-
);
|
|
560
|
-
out = out.replace(BRN_CANDIDATE_PATTERN, (match2) => {
|
|
561
|
-
const digits = match2.replace(/-/g, "");
|
|
562
|
-
return passesBrnChecksum(digits) ? REDACTED_BODY_VALUE : match2;
|
|
563
|
-
});
|
|
564
|
-
out = out.replace(KR_MOBILE_PATTERN, REDACTED_BODY_VALUE);
|
|
565
|
-
return out === text ? text : out;
|
|
566
|
-
}
|
|
567
|
-
function redactUrlSecrets(url) {
|
|
568
|
-
if (!url) return url;
|
|
569
|
-
const queryStart = url.indexOf("?");
|
|
570
|
-
const fragmentStart = url.indexOf("#");
|
|
571
|
-
if (queryStart < 0 && fragmentStart < 0) {
|
|
572
|
-
return redactSensitiveValues(url);
|
|
573
|
-
}
|
|
574
|
-
const hasQuery = queryStart >= 0 && (fragmentStart < 0 || queryStart < fragmentStart);
|
|
575
|
-
const base = url.slice(0, hasQuery ? queryStart : fragmentStart);
|
|
576
|
-
const query = hasQuery ? url.slice(queryStart + 1, fragmentStart >= 0 ? fragmentStart : void 0) : "";
|
|
577
|
-
const fragment = fragmentStart >= 0 ? url.slice(fragmentStart + 1) : "";
|
|
578
|
-
const redactedQuery = redactKeyValuePairs(query);
|
|
579
|
-
const redactedFragment = fragment.includes("=") ? redactKeyValuePairs(fragment) : fragment;
|
|
580
|
-
const redactedBase = redactSensitiveValues(base);
|
|
581
|
-
if (redactedBase === base && redactedQuery === query && redactedFragment === fragment) {
|
|
582
|
-
return url;
|
|
583
|
-
}
|
|
584
|
-
return redactedBase + (hasQuery ? `?${redactedQuery}` : "") + (fragmentStart >= 0 ? `#${redactedFragment}` : "");
|
|
585
|
-
}
|
|
586
|
-
function redactSensitiveText(text) {
|
|
587
|
-
if (!text) return text;
|
|
588
|
-
return redactSensitiveValues(redactUrlSecrets(redactSensitiveBody(text)));
|
|
589
|
-
}
|
|
590
640
|
const META_EVENT_TYPE = 4;
|
|
591
641
|
const FULL_SNAPSHOT_EVENT_TYPE = 2;
|
|
592
642
|
const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
|
|
@@ -667,7 +717,13 @@ const RRWEB_VERSION = "2.1.0";
|
|
|
667
717
|
const SESSION_REPLAY_WINDOW_MS = 12e4;
|
|
668
718
|
const SESSION_REPLAY_CHECKOUT_MS = 6e4;
|
|
669
719
|
const REPLAY_MASK_VALUE = "[redacted]";
|
|
670
|
-
|
|
720
|
+
const UNMASKED_INPUT_TYPES = /* @__PURE__ */ new Set(["range", "color", "checkbox", "radio"]);
|
|
721
|
+
function maskReplayInput(text, element) {
|
|
722
|
+
var _a;
|
|
723
|
+
const type = (_a = element == null ? void 0 : element.type) == null ? void 0 : _a.toLowerCase();
|
|
724
|
+
if (type && UNMASKED_INPUT_TYPES.has(type)) {
|
|
725
|
+
return text;
|
|
726
|
+
}
|
|
671
727
|
return text ? REPLAY_MASK_VALUE : text;
|
|
672
728
|
}
|
|
673
729
|
function createMaskTextFn(fullMaskSelector, unmaskSelector) {
|
|
@@ -683,19 +739,12 @@ function createMaskTextFn(fullMaskSelector, unmaskSelector) {
|
|
|
683
739
|
};
|
|
684
740
|
}
|
|
685
741
|
const IDLE_CHECKOUT_POLL_MS = 5e3;
|
|
686
|
-
function spanSeconds(segments) {
|
|
687
|
-
var _a, _b;
|
|
688
|
-
const events = flattenEvents(segments);
|
|
689
|
-
const first = (_a = events[0]) == null ? void 0 : _a.timestamp;
|
|
690
|
-
const last = (_b = events[events.length - 1]) == null ? void 0 : _b.timestamp;
|
|
691
|
-
if (first === void 0 || last === void 0) return 0;
|
|
692
|
-
return Math.round((last - first) / 1e3);
|
|
693
|
-
}
|
|
694
742
|
class RingBufferRecorder {
|
|
695
743
|
constructor(options = {}) {
|
|
696
744
|
this.options = options;
|
|
697
745
|
this.segments = createSegments();
|
|
698
746
|
this.stopFn = null;
|
|
747
|
+
this.stopCanvasTracking = null;
|
|
699
748
|
this.idleCheckoutTimer = null;
|
|
700
749
|
this.visibilityListener = null;
|
|
701
750
|
this.tailCutoff = null;
|
|
@@ -746,10 +795,13 @@ class RingBufferRecorder {
|
|
|
746
795
|
this.segments = createSegments();
|
|
747
796
|
const stop = recordFn({
|
|
748
797
|
emit: (event, isCheckout) => {
|
|
749
|
-
var _a, _b;
|
|
798
|
+
var _a, _b, _c;
|
|
750
799
|
this.segments = appendEvent(this.segments, event, Boolean(isCheckout));
|
|
751
800
|
this.segments = trimSegments(this.segments, this.windowMs, now(), this.checkoutEveryMs);
|
|
752
|
-
|
|
801
|
+
if (isCheckout) {
|
|
802
|
+
(_a = this.stopCanvasTracking) == null ? void 0 : _a.captureBaseline();
|
|
803
|
+
}
|
|
804
|
+
(_c = (_b = this.options).onChange) == null ? void 0 : _c.call(_b);
|
|
753
805
|
},
|
|
754
806
|
checkoutEveryNms: this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS,
|
|
755
807
|
maskAllInputs: cfg.maskAllInputs,
|
|
@@ -761,6 +813,9 @@ class RingBufferRecorder {
|
|
|
761
813
|
// rrweb 옵션 타입이 버전별로 달라 캐스팅한다.
|
|
762
814
|
});
|
|
763
815
|
this.stopFn = typeof stop === "function" ? stop : null;
|
|
816
|
+
if (this.options.trackCanvasInteractions !== false) {
|
|
817
|
+
this.stopCanvasTracking = startCanvasInteractionTracking();
|
|
818
|
+
}
|
|
764
819
|
this.idleCheckoutTimer = setInterval(() => this.forceCheckoutIfIdle(), IDLE_CHECKOUT_POLL_MS);
|
|
765
820
|
if (typeof document !== "undefined") {
|
|
766
821
|
this.visibilityListener = () => this.forceCheckoutIfIdle();
|
|
@@ -768,9 +823,11 @@ class RingBufferRecorder {
|
|
|
768
823
|
}
|
|
769
824
|
}
|
|
770
825
|
stop() {
|
|
771
|
-
var _a;
|
|
826
|
+
var _a, _b;
|
|
772
827
|
(_a = this.stopFn) == null ? void 0 : _a.call(this);
|
|
773
828
|
this.stopFn = null;
|
|
829
|
+
(_b = this.stopCanvasTracking) == null ? void 0 : _b.call(this);
|
|
830
|
+
this.stopCanvasTracking = null;
|
|
774
831
|
if (this.idleCheckoutTimer !== null) {
|
|
775
832
|
clearInterval(this.idleCheckoutTimer);
|
|
776
833
|
this.idleCheckoutTimer = null;
|
|
@@ -837,7 +894,6 @@ class RingBufferRecorder {
|
|
|
837
894
|
return serializeSnapshot({
|
|
838
895
|
segments,
|
|
839
896
|
redactEvents: redactReplayEvents,
|
|
840
|
-
windowSeconds: spanSeconds(segments),
|
|
841
897
|
rrwebVersion: RRWEB_VERSION,
|
|
842
898
|
privacyConfigVersion: cfg.version
|
|
843
899
|
});
|
|
@@ -870,17 +926,31 @@ function withoutBody(log) {
|
|
|
870
926
|
}
|
|
871
927
|
return stripped;
|
|
872
928
|
}
|
|
929
|
+
function withoutFrameData(connection) {
|
|
930
|
+
if (connection.frames.length === 0) return connection;
|
|
931
|
+
return {
|
|
932
|
+
...connection,
|
|
933
|
+
frames: connection.frames.map((frame) => {
|
|
934
|
+
if (frame.data === void 0) return frame;
|
|
935
|
+
const stripped = { ...frame };
|
|
936
|
+
delete stripped.data;
|
|
937
|
+
return stripped;
|
|
938
|
+
})
|
|
939
|
+
};
|
|
940
|
+
}
|
|
873
941
|
function trimLogBackupPayload(payload, budgetBytes = LOG_BACKUP_BUDGET_BYTES) {
|
|
942
|
+
var _a;
|
|
874
943
|
if (payloadSizeBytes(payload) <= budgetBytes) return payload;
|
|
875
944
|
let trimmed = {
|
|
876
945
|
network: payload.network.map(withoutBody),
|
|
877
|
-
console: payload.console
|
|
946
|
+
console: payload.console,
|
|
947
|
+
socket: (_a = payload.socket) == null ? void 0 : _a.map(withoutFrameData)
|
|
878
948
|
};
|
|
879
949
|
while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.network.length > 0) {
|
|
880
|
-
trimmed = { network: trimmed.network.slice(1)
|
|
950
|
+
trimmed = { ...trimmed, network: trimmed.network.slice(1) };
|
|
881
951
|
}
|
|
882
952
|
while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.console.length > 0) {
|
|
883
|
-
trimmed = {
|
|
953
|
+
trimmed = { ...trimmed, console: trimmed.console.slice(1) };
|
|
884
954
|
}
|
|
885
955
|
return trimmed;
|
|
886
956
|
}
|
|
@@ -1029,13 +1099,29 @@ const DB_NAME = "issue-sticker-replay";
|
|
|
1029
1099
|
const DB_VERSION = 1;
|
|
1030
1100
|
const STORE_NAME = "segments";
|
|
1031
1101
|
const REPLAY_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1102
|
+
const REPLAY_BACKUP_SIZE_BUDGET = 32 * 1024 * 1024;
|
|
1103
|
+
const eventSizes = /* @__PURE__ */ new WeakMap();
|
|
1104
|
+
function sizeOfEvent(event) {
|
|
1105
|
+
const cached = eventSizes.get(event);
|
|
1106
|
+
if (cached !== void 0) return cached;
|
|
1107
|
+
let size;
|
|
1108
|
+
try {
|
|
1109
|
+
size = JSON.stringify(event).length;
|
|
1110
|
+
} catch {
|
|
1111
|
+
size = Number.MAX_SAFE_INTEGER;
|
|
1112
|
+
}
|
|
1113
|
+
eventSizes.set(event, size);
|
|
1114
|
+
return size;
|
|
1115
|
+
}
|
|
1116
|
+
function sizeOfSegments(segments) {
|
|
1117
|
+
return segments.reduce(
|
|
1118
|
+
(total, segment) => total + segment.events.reduce((sum, event) => sum + sizeOfEvent(event), 0),
|
|
1119
|
+
0
|
|
1120
|
+
);
|
|
1035
1121
|
}
|
|
1036
|
-
function trimReplayBackup(segments,
|
|
1122
|
+
function trimReplayBackup(segments, sizeBudget = REPLAY_BACKUP_SIZE_BUDGET) {
|
|
1037
1123
|
let trimmed = segments;
|
|
1038
|
-
while (trimmed.length > 1 &&
|
|
1124
|
+
while (trimmed.length > 1 && sizeOfSegments(trimmed) > sizeBudget) {
|
|
1039
1125
|
trimmed = trimmed.slice(1);
|
|
1040
1126
|
}
|
|
1041
1127
|
return trimmed;
|
|
@@ -1236,6 +1322,8 @@ function createReplaySession(options = {}) {
|
|
|
1236
1322
|
backup.schedule();
|
|
1237
1323
|
};
|
|
1238
1324
|
const recorder = options.recorder ?? new RingBufferRecorder({
|
|
1325
|
+
...options.trackCanvasInteractions === false ? { trackCanvasInteractions: false } : {},
|
|
1326
|
+
...options.windowMs === void 0 ? {} : { windowMs: options.windowMs },
|
|
1239
1327
|
privacyConfig: options.privacyConfig,
|
|
1240
1328
|
onChange: scheduleBackup
|
|
1241
1329
|
});
|
|
@@ -1318,7 +1406,11 @@ class SessionReplayCaptureController {
|
|
|
1318
1406
|
this.setSnapshot({ available: false, canAttach: false, error: null });
|
|
1319
1407
|
return;
|
|
1320
1408
|
}
|
|
1321
|
-
const
|
|
1409
|
+
const windowSeconds = sessionReplay == null ? void 0 : sessionReplay.windowSeconds;
|
|
1410
|
+
const recorder = this.createRecorder({
|
|
1411
|
+
...(sessionReplay == null ? void 0 : sessionReplay.privacyConfig) ? { privacyConfig: sessionReplay.privacyConfig } : {},
|
|
1412
|
+
...typeof windowSeconds === "number" && windowSeconds > 0 ? { windowMs: windowSeconds * 1e3 } : {}
|
|
1413
|
+
});
|
|
1322
1414
|
recorder.start();
|
|
1323
1415
|
this.recorder = recorder;
|
|
1324
1416
|
this.setSnapshot({ available: true, canAttach, error: null });
|
|
@@ -1354,8 +1446,8 @@ class SessionReplayCaptureController {
|
|
|
1354
1446
|
this.stopRecorder();
|
|
1355
1447
|
this.listeners.clear();
|
|
1356
1448
|
}
|
|
1357
|
-
createRecorder(
|
|
1358
|
-
return this.options.createRecorder ? this.options.createRecorder(
|
|
1449
|
+
createRecorder(config) {
|
|
1450
|
+
return this.options.createRecorder ? this.options.createRecorder(config) : createReplaySession(config);
|
|
1359
1451
|
}
|
|
1360
1452
|
stopRecorder() {
|
|
1361
1453
|
var _a;
|
|
@@ -1370,179 +1462,6 @@ class SessionReplayCaptureController {
|
|
|
1370
1462
|
this.listeners.forEach((listener) => listener(this.snapshot));
|
|
1371
1463
|
}
|
|
1372
1464
|
}
|
|
1373
|
-
function forEachElementDeep(root, callback) {
|
|
1374
|
-
if (root instanceof Element) {
|
|
1375
|
-
callback(root);
|
|
1376
|
-
if (root.shadowRoot) {
|
|
1377
|
-
forEachElementDeep(root.shadowRoot, callback);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
Array.from(root.children).forEach((child) => {
|
|
1381
|
-
forEachElementDeep(child, callback);
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
const SCREENSHOT_FULL_MASK_SELECTOR = SESSION_REPLAY_FULL_MASK_SELECTOR;
|
|
1385
|
-
const SCREENSHOT_BLOCK_SELECTOR = SESSION_REPLAY_BLOCK_SELECTOR;
|
|
1386
|
-
const SCREENSHOT_MASK_BACKGROUND = "rgba(148, 148, 148, 0.28)";
|
|
1387
|
-
const SCREENSHOT_BLOCK_BACKGROUND = "rgba(148, 148, 148, 0.85)";
|
|
1388
|
-
const MASK_LABEL_WIDTH = 44;
|
|
1389
|
-
const MASK_LABEL_HEIGHT = 16;
|
|
1390
|
-
const MASK_LABEL_IMAGE = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(
|
|
1391
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${MASK_LABEL_WIDTH}" height="${MASK_LABEL_HEIGHT}" viewBox="0 0 ${MASK_LABEL_WIDTH} ${MASK_LABEL_HEIGHT}"><text x="${MASK_LABEL_WIDTH / 2}" y="12" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#262626">가려짐</text></svg>`
|
|
1392
|
-
)}")`;
|
|
1393
|
-
const MASK_LABEL_MIN_WIDTH = MASK_LABEL_WIDTH + 8;
|
|
1394
|
-
const MASK_LABEL_MIN_HEIGHT = MASK_LABEL_HEIGHT + 8;
|
|
1395
|
-
const TEXT_HIDING_PROPERTIES = [
|
|
1396
|
-
["color", "transparent"],
|
|
1397
|
-
["-webkit-text-fill-color", "transparent"],
|
|
1398
|
-
["text-shadow", "none"],
|
|
1399
|
-
["fill", "transparent"]
|
|
1400
|
-
];
|
|
1401
|
-
const REPLACED_MEDIA_SELECTOR = "img, svg, video, canvas, picture, iframe, object, embed";
|
|
1402
|
-
const VALUE_ELEMENT_SELECTOR = "input, textarea, select";
|
|
1403
|
-
const SENSITIVE_AUTOCOMPLETE_TOKENS = /* @__PURE__ */ new Set([
|
|
1404
|
-
// 자격증명
|
|
1405
|
-
"current-password",
|
|
1406
|
-
"new-password",
|
|
1407
|
-
"one-time-code",
|
|
1408
|
-
// 결제
|
|
1409
|
-
"cc-number",
|
|
1410
|
-
"cc-exp",
|
|
1411
|
-
"cc-exp-month",
|
|
1412
|
-
"cc-exp-year",
|
|
1413
|
-
"cc-csc",
|
|
1414
|
-
"cc-name",
|
|
1415
|
-
"cc-type",
|
|
1416
|
-
"cc-given-name",
|
|
1417
|
-
"cc-additional-name",
|
|
1418
|
-
"cc-family-name",
|
|
1419
|
-
// 신원
|
|
1420
|
-
"name",
|
|
1421
|
-
"given-name",
|
|
1422
|
-
"additional-name",
|
|
1423
|
-
"family-name",
|
|
1424
|
-
"honorific-prefix",
|
|
1425
|
-
"honorific-suffix",
|
|
1426
|
-
"nickname",
|
|
1427
|
-
"bday",
|
|
1428
|
-
"bday-day",
|
|
1429
|
-
"bday-month",
|
|
1430
|
-
"bday-year",
|
|
1431
|
-
"sex",
|
|
1432
|
-
// 주소
|
|
1433
|
-
"street-address",
|
|
1434
|
-
"address-line1",
|
|
1435
|
-
"address-line2",
|
|
1436
|
-
"address-line3",
|
|
1437
|
-
"postal-code",
|
|
1438
|
-
// 연락처
|
|
1439
|
-
"tel",
|
|
1440
|
-
"tel-national",
|
|
1441
|
-
"tel-local",
|
|
1442
|
-
"tel-area-code",
|
|
1443
|
-
"email",
|
|
1444
|
-
"impp"
|
|
1445
|
-
]);
|
|
1446
|
-
function redactScreenshotText(text) {
|
|
1447
|
-
return redactEmails(redactSensitiveValues(text));
|
|
1448
|
-
}
|
|
1449
|
-
function matchesSafely(element, selector) {
|
|
1450
|
-
try {
|
|
1451
|
-
return element.matches(selector);
|
|
1452
|
-
} catch {
|
|
1453
|
-
return false;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
function closestSafely(element, selector) {
|
|
1457
|
-
if (!element) return null;
|
|
1458
|
-
try {
|
|
1459
|
-
return element.closest(selector);
|
|
1460
|
-
} catch {
|
|
1461
|
-
return null;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
function hasSensitiveValueHint(element) {
|
|
1465
|
-
var _a;
|
|
1466
|
-
if (((_a = element.getAttribute("type")) == null ? void 0 : _a.toLowerCase()) === "password") {
|
|
1467
|
-
return true;
|
|
1468
|
-
}
|
|
1469
|
-
const autocomplete = element.getAttribute("autocomplete");
|
|
1470
|
-
if (!autocomplete) return false;
|
|
1471
|
-
return autocomplete.toLowerCase().split(/\s+/).some((token) => SENSITIVE_AUTOCOMPLETE_TOKENS.has(token));
|
|
1472
|
-
}
|
|
1473
|
-
const overwriteProperty = (element, name, value) => {
|
|
1474
|
-
const style = element.style;
|
|
1475
|
-
style == null ? void 0 : style.setProperty(name, value, "important");
|
|
1476
|
-
};
|
|
1477
|
-
function coverMaskedRegion(region, setProperty) {
|
|
1478
|
-
setProperty(region, "background-color", SCREENSHOT_MASK_BACKGROUND);
|
|
1479
|
-
forEachElementDeep(region, (node) => {
|
|
1480
|
-
if (!isElementFullyMasked(node)) return;
|
|
1481
|
-
TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(node, name, value));
|
|
1482
|
-
if (matchesSafely(node, REPLACED_MEDIA_SELECTOR)) {
|
|
1483
|
-
setProperty(node, "visibility", "hidden");
|
|
1484
|
-
}
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
function parsePixels(value) {
|
|
1488
|
-
const parsed = Number.parseFloat(value ?? "");
|
|
1489
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
1490
|
-
}
|
|
1491
|
-
function fitsMaskLabel(region) {
|
|
1492
|
-
const rect = region.getBoundingClientRect();
|
|
1493
|
-
const style = region.style;
|
|
1494
|
-
const width = rect.width || parsePixels(style == null ? void 0 : style.width);
|
|
1495
|
-
const height = rect.height || parsePixels(style == null ? void 0 : style.height);
|
|
1496
|
-
return width >= MASK_LABEL_MIN_WIDTH && height >= MASK_LABEL_MIN_HEIGHT;
|
|
1497
|
-
}
|
|
1498
|
-
function coverBlockedRegion(region, setProperty) {
|
|
1499
|
-
const labeled = fitsMaskLabel(region);
|
|
1500
|
-
setProperty(region, "visibility", "visible");
|
|
1501
|
-
setProperty(region, "background-color", SCREENSHOT_BLOCK_BACKGROUND);
|
|
1502
|
-
setProperty(region, "background-image", labeled ? MASK_LABEL_IMAGE : "none");
|
|
1503
|
-
if (labeled) {
|
|
1504
|
-
setProperty(region, "background-repeat", "no-repeat");
|
|
1505
|
-
setProperty(region, "background-position", "center");
|
|
1506
|
-
setProperty(region, "background-size", "auto");
|
|
1507
|
-
}
|
|
1508
|
-
TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(region, name, value));
|
|
1509
|
-
setProperty(region, "pointer-events", "none");
|
|
1510
|
-
forEachElementDeep(region, (descendant) => {
|
|
1511
|
-
if (descendant !== region) {
|
|
1512
|
-
setProperty(descendant, "visibility", "hidden");
|
|
1513
|
-
}
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
function coverPrivateRegions(root, setProperty) {
|
|
1517
|
-
const blocked = [];
|
|
1518
|
-
const masked = [];
|
|
1519
|
-
forEachElementDeep(root, (element) => {
|
|
1520
|
-
if (matchesSafely(element, SCREENSHOT_BLOCK_SELECTOR)) {
|
|
1521
|
-
blocked.push(element);
|
|
1522
|
-
return;
|
|
1523
|
-
}
|
|
1524
|
-
if (matchesSafely(element, SCREENSHOT_FULL_MASK_SELECTOR) && isElementFullyMasked(element)) {
|
|
1525
|
-
masked.push(element);
|
|
1526
|
-
}
|
|
1527
|
-
});
|
|
1528
|
-
masked.forEach((region) => coverMaskedRegion(region, setProperty));
|
|
1529
|
-
blocked.forEach((region) => coverBlockedRegion(region, setProperty));
|
|
1530
|
-
}
|
|
1531
|
-
function applyScreenshotPrivacyMask(clonedRoot) {
|
|
1532
|
-
coverPrivateRegions(clonedRoot, overwriteProperty);
|
|
1533
|
-
const walker = clonedRoot.ownerDocument.createTreeWalker(clonedRoot, NodeFilter.SHOW_TEXT);
|
|
1534
|
-
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
1535
|
-
const text = node.nodeValue;
|
|
1536
|
-
if (!text) continue;
|
|
1537
|
-
node.nodeValue = redactScreenshotText(text);
|
|
1538
|
-
}
|
|
1539
|
-
clonedRoot.querySelectorAll(VALUE_ELEMENT_SELECTOR).forEach((element) => {
|
|
1540
|
-
const value = element.getAttribute("value");
|
|
1541
|
-
if (!value) return;
|
|
1542
|
-
const hidden = closestSafely(element, SCREENSHOT_BLOCK_SELECTOR) !== null || isElementFullyMasked(element) || hasSensitiveValueHint(element);
|
|
1543
|
-
element.setAttribute("value", hidden ? "" : redactScreenshotText(value));
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
1465
|
const SCREENSHOT_DATA_URL_QUALITY = 0.95;
|
|
1547
1466
|
let nextCaptureRunId = 0;
|
|
1548
1467
|
function createCaptureAttributeNames() {
|
|
@@ -1761,10 +1680,18 @@ function blobToDataUrl(blob) {
|
|
|
1761
1680
|
reader.readAsDataURL(blob);
|
|
1762
1681
|
});
|
|
1763
1682
|
}
|
|
1683
|
+
function captureOrigin(element) {
|
|
1684
|
+
if (element === document.documentElement || element === document.body) {
|
|
1685
|
+
return { x: 0, y: 0 };
|
|
1686
|
+
}
|
|
1687
|
+
const rect = element.getBoundingClientRect();
|
|
1688
|
+
return { x: rect.left, y: rect.top };
|
|
1689
|
+
}
|
|
1764
1690
|
async function captureWithModernScreenshot(options) {
|
|
1765
1691
|
const { domToBlob } = await loadModernScreenshot();
|
|
1766
1692
|
const { element, excludeSelectors = [], width, height, scale = getCaptureScale() } = options;
|
|
1767
1693
|
await waitForDocumentFonts();
|
|
1694
|
+
const canvasFrames = await grabCanvasFrames(element, excludeSelectors);
|
|
1768
1695
|
const attributeNames = createCaptureAttributeNames();
|
|
1769
1696
|
const stickySnapshot = createModernStickySnapshot(element, attributeNames);
|
|
1770
1697
|
const excludeSnapshot = createExcludeElementSnapshot(element, excludeSelectors, attributeNames);
|
|
@@ -1791,8 +1718,21 @@ async function captureWithModernScreenshot(options) {
|
|
|
1791
1718
|
if (height) {
|
|
1792
1719
|
captureOptions.height = height;
|
|
1793
1720
|
}
|
|
1794
|
-
|
|
1721
|
+
const captured = await domToBlob(element, captureOptions);
|
|
1722
|
+
if (canvasFrames.length === 0) {
|
|
1723
|
+
return await blobToDataUrl(captured);
|
|
1724
|
+
}
|
|
1725
|
+
return await blobToDataUrl(
|
|
1726
|
+
await overlayCanvasFrames(
|
|
1727
|
+
captured,
|
|
1728
|
+
canvasFrames,
|
|
1729
|
+
captureOrigin(element),
|
|
1730
|
+
width ?? element.getBoundingClientRect().width,
|
|
1731
|
+
SCREENSHOT_DATA_URL_QUALITY
|
|
1732
|
+
)
|
|
1733
|
+
);
|
|
1795
1734
|
} finally {
|
|
1735
|
+
canvasFrames.forEach((frame) => frame.bitmap.close());
|
|
1796
1736
|
excludeSnapshot.cleanup();
|
|
1797
1737
|
stickySnapshot.cleanup();
|
|
1798
1738
|
}
|
|
@@ -1877,7 +1817,7 @@ async function calculateSha256Hex(file) {
|
|
|
1877
1817
|
}
|
|
1878
1818
|
function normalizeUploadMimeType(mimeType, mediaType) {
|
|
1879
1819
|
var _a;
|
|
1880
|
-
const fallback = mediaType === "screenshot" ? "image/webp" : mediaType === "replay" ? "application/gzip" : "video/webm";
|
|
1820
|
+
const fallback = mediaType === "screenshot" ? "image/webp" : mediaType === "replay" || mediaType === "socket" ? "application/gzip" : "video/webm";
|
|
1881
1821
|
const normalized = (_a = (mimeType || fallback).split(";", 1)[0]) == null ? void 0 : _a.trim().toLowerCase();
|
|
1882
1822
|
return normalized && normalized.length > 0 ? normalized : fallback;
|
|
1883
1823
|
}
|
|
@@ -4188,8 +4128,8 @@ function pristineFetch(input, init) {
|
|
|
4188
4128
|
}
|
|
4189
4129
|
return typeof window !== "undefined" ? window.fetch(input, init) : fetch(input, init);
|
|
4190
4130
|
}
|
|
4191
|
-
const INSTRUMENTATION_FAILURE_LIMIT = 5;
|
|
4192
|
-
const REPATCH_LIMIT = 2;
|
|
4131
|
+
const INSTRUMENTATION_FAILURE_LIMIT$1 = 5;
|
|
4132
|
+
const REPATCH_LIMIT$1 = 2;
|
|
4193
4133
|
function setInternalField(target, key, value) {
|
|
4194
4134
|
Object.defineProperty(target, key, {
|
|
4195
4135
|
value,
|
|
@@ -4286,7 +4226,7 @@ function omitTooLargeResponseBody(sizeBytes) {
|
|
|
4286
4226
|
responseBodySizeBytes: sizeBytes
|
|
4287
4227
|
};
|
|
4288
4228
|
}
|
|
4289
|
-
function bytesToBase64(bytes) {
|
|
4229
|
+
function bytesToBase64$1(bytes) {
|
|
4290
4230
|
let binary = "";
|
|
4291
4231
|
const chunkSize = 32768;
|
|
4292
4232
|
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
@@ -4296,7 +4236,7 @@ function bytesToBase64(bytes) {
|
|
|
4296
4236
|
return btoa(binary);
|
|
4297
4237
|
}
|
|
4298
4238
|
async function arrayBufferToBase64(arrayBuffer) {
|
|
4299
|
-
return bytesToBase64(new Uint8Array(arrayBuffer));
|
|
4239
|
+
return bytesToBase64$1(new Uint8Array(arrayBuffer));
|
|
4300
4240
|
}
|
|
4301
4241
|
function getResponseDetailLevel(response) {
|
|
4302
4242
|
if (response.type === "opaque" || response.type === "opaqueredirect") return "opaque";
|
|
@@ -4413,7 +4353,7 @@ class NetworkCollector {
|
|
|
4413
4353
|
ensurePatched() {
|
|
4414
4354
|
if (!this.running) return;
|
|
4415
4355
|
if (window.fetch === this.patchedFetch) return;
|
|
4416
|
-
if (this.repatchCount >= REPATCH_LIMIT) return;
|
|
4356
|
+
if (this.repatchCount >= REPATCH_LIMIT$1) return;
|
|
4417
4357
|
this.repatchCount += 1;
|
|
4418
4358
|
this.interceptFetch();
|
|
4419
4359
|
}
|
|
@@ -4459,7 +4399,7 @@ class NetworkCollector {
|
|
|
4459
4399
|
*/
|
|
4460
4400
|
recordInstrumentationFailure() {
|
|
4461
4401
|
this.instrumentationFailures += 1;
|
|
4462
|
-
if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT) {
|
|
4402
|
+
if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT$1) {
|
|
4463
4403
|
this.stop();
|
|
4464
4404
|
}
|
|
4465
4405
|
}
|
|
@@ -4681,6 +4621,422 @@ class NetworkCollector {
|
|
|
4681
4621
|
this.patchedXHR = null;
|
|
4682
4622
|
}
|
|
4683
4623
|
}
|
|
4624
|
+
const SOCKET_FRAME_WINDOW_MS = 12e4;
|
|
4625
|
+
const SOCKET_FRAME_BUDGET_BYTES = 2 * 1024 * 1024;
|
|
4626
|
+
const SOCKET_FRAME_MAX_BYTES = 8 * 1024;
|
|
4627
|
+
const SOCKET_CONNECTION_LIMIT = 32;
|
|
4628
|
+
const SOCKET_TRANSCRIPT_MAX_BYTES = 5 * 1024 * 1024;
|
|
4629
|
+
const INSTRUMENTATION_FAILURE_LIMIT = 5;
|
|
4630
|
+
const REPATCH_LIMIT = 2;
|
|
4631
|
+
function toComparableOrigin(url) {
|
|
4632
|
+
return url.replace(/^wss:\/\//i, "https://").replace(/^ws:\/\//i, "http://");
|
|
4633
|
+
}
|
|
4634
|
+
function bytesToBase64(bytes) {
|
|
4635
|
+
let binary = "";
|
|
4636
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
4637
|
+
return btoa(binary);
|
|
4638
|
+
}
|
|
4639
|
+
function toFrameData(data, dir) {
|
|
4640
|
+
if (typeof data === "string") {
|
|
4641
|
+
return { opcode: "text", size: new TextEncoder().encode(data).length, text: data };
|
|
4642
|
+
}
|
|
4643
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
4644
|
+
return { opcode: "binary", size: data.size, bytes: new Uint8Array(0) };
|
|
4645
|
+
}
|
|
4646
|
+
if (data instanceof ArrayBuffer) {
|
|
4647
|
+
const bytes = new Uint8Array(data);
|
|
4648
|
+
return { opcode: "binary", size: bytes.length, bytes };
|
|
4649
|
+
}
|
|
4650
|
+
if (ArrayBuffer.isView(data)) {
|
|
4651
|
+
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
4652
|
+
return { opcode: "binary", size: bytes.length, bytes };
|
|
4653
|
+
}
|
|
4654
|
+
if (dir === "send") {
|
|
4655
|
+
const text = String(data);
|
|
4656
|
+
return { opcode: "text", size: new TextEncoder().encode(text).length, text };
|
|
4657
|
+
}
|
|
4658
|
+
throw new TypeError("WebSocketCollector: 지원하지 않는 프레임 데이터 형식");
|
|
4659
|
+
}
|
|
4660
|
+
const MASK_PRESLICE_MARGIN_BYTES = SOCKET_FRAME_MAX_BYTES * 4;
|
|
4661
|
+
function bytesNeededForBase64Length(charLength) {
|
|
4662
|
+
return Math.ceil(charLength * 3 / 4) + 3;
|
|
4663
|
+
}
|
|
4664
|
+
function maskFrameText(text) {
|
|
4665
|
+
return redactSensitiveValues(redactSensitiveBody(text));
|
|
4666
|
+
}
|
|
4667
|
+
class WebSocketCollector {
|
|
4668
|
+
constructor(apiBaseUrl, options) {
|
|
4669
|
+
this.connections = [];
|
|
4670
|
+
this.originalWebSocket = null;
|
|
4671
|
+
this.patchedWebSocket = null;
|
|
4672
|
+
this.originalSend = null;
|
|
4673
|
+
this.patchedSend = null;
|
|
4674
|
+
this.running = false;
|
|
4675
|
+
this.nextId = 1;
|
|
4676
|
+
this.totalBytes = 0;
|
|
4677
|
+
this.instrumentationFailures = 0;
|
|
4678
|
+
this.repatchCount = 0;
|
|
4679
|
+
this.sink = null;
|
|
4680
|
+
this.logByConnectionId = /* @__PURE__ */ new Map();
|
|
4681
|
+
this.apiBaseUrl = typeof apiBaseUrl === "string" && apiBaseUrl.trim().length > 0 ? apiBaseUrl.trim() : null;
|
|
4682
|
+
this.onChange = (options == null ? void 0 : options.onChange) ?? null;
|
|
4683
|
+
}
|
|
4684
|
+
start() {
|
|
4685
|
+
if (this.running) return;
|
|
4686
|
+
if (typeof globalThis.WebSocket !== "function") return;
|
|
4687
|
+
this.running = true;
|
|
4688
|
+
this.instrumentationFailures = 0;
|
|
4689
|
+
this.patch();
|
|
4690
|
+
}
|
|
4691
|
+
stop() {
|
|
4692
|
+
if (!this.running) return;
|
|
4693
|
+
this.running = false;
|
|
4694
|
+
this.unpatch();
|
|
4695
|
+
}
|
|
4696
|
+
/**
|
|
4697
|
+
* 우리 래퍼가 아직 걸려 있는지 확인하고, 아니면 다시 붙인다.
|
|
4698
|
+
*
|
|
4699
|
+
* NetworkCollector.ensurePatched 와 같은 이유다 — 다른 계측이 `window.WebSocket` 을
|
|
4700
|
+
* 통째로 갈아치우면 수집이 조용히 멈춘다.
|
|
4701
|
+
*/
|
|
4702
|
+
ensurePatched() {
|
|
4703
|
+
if (!this.running) return;
|
|
4704
|
+
if (globalThis.WebSocket === this.patchedWebSocket) return;
|
|
4705
|
+
if (this.repatchCount >= REPATCH_LIMIT) return;
|
|
4706
|
+
this.repatchCount += 1;
|
|
4707
|
+
this.patch();
|
|
4708
|
+
}
|
|
4709
|
+
/**
|
|
4710
|
+
* 기존 네트워크 로그 목록에 소켓 연결을 한 줄로 흘려보낸다.
|
|
4711
|
+
*
|
|
4712
|
+
* `NetworkCollector.ingest` 를 그대로 받는다 — 새 읽기 경로를 만들지 않고 로그를 읽는
|
|
4713
|
+
* 세 소비자(SDK content·SDK headless·익스텐션 iife)가 이미 보는 목록에 얹는다.
|
|
4714
|
+
*/
|
|
4715
|
+
bindNetworkLogSink(ingest) {
|
|
4716
|
+
this.sink = ingest;
|
|
4717
|
+
}
|
|
4718
|
+
/**
|
|
4719
|
+
* 연결이 열릴 때 줄을 한 번만 만들어 싱크로 내보낸다.
|
|
4720
|
+
*
|
|
4721
|
+
* 이후 프레임·닫힘 갱신은 `syncConnectionLog` 가 이 객체를 그대로 고친다 — 싱크
|
|
4722
|
+
* 소비자는 배열에 새 항목이 늘지 않고 이미 받은 참조가 갱신되는 것만 본다.
|
|
4723
|
+
*/
|
|
4724
|
+
emitConnectionLog(connection) {
|
|
4725
|
+
if (!this.sink) return;
|
|
4726
|
+
const log = {
|
|
4727
|
+
url: connection.url,
|
|
4728
|
+
// 핸드셰이크는 GET 으로 나가고 성공이면 101 이다.
|
|
4729
|
+
method: "GET",
|
|
4730
|
+
statusCode: 101,
|
|
4731
|
+
timestamp: connection.openedAt,
|
|
4732
|
+
type: "websocket",
|
|
4733
|
+
socketConnectionId: connection.id,
|
|
4734
|
+
frameCount: 0,
|
|
4735
|
+
frameBytes: 0,
|
|
4736
|
+
droppedFrames: 0
|
|
4737
|
+
};
|
|
4738
|
+
this.logByConnectionId.set(connection.id, log);
|
|
4739
|
+
this.sink(log);
|
|
4740
|
+
}
|
|
4741
|
+
/**
|
|
4742
|
+
* `emitConnectionLog` 가 내보낸 줄을 연결의 최신 상태로 고친다.
|
|
4743
|
+
*
|
|
4744
|
+
* `frameCount` 는 버린 프레임까지 센다("실제로 몇 건 오갔나") — `frameBytes` 는 지금
|
|
4745
|
+
* 보관 중인 프레임의 크기 합이라 기준이 다르다. 버린 프레임의 바이트는 이미 사라졌으니
|
|
4746
|
+
* 더할 수 없다.
|
|
4747
|
+
*/
|
|
4748
|
+
syncConnectionLog(connection) {
|
|
4749
|
+
const log = this.logByConnectionId.get(connection.id);
|
|
4750
|
+
if (!log) return;
|
|
4751
|
+
log.frameCount = connection.frames.length + connection.droppedFrames;
|
|
4752
|
+
log.frameBytes = connection.frames.reduce((sum, frame) => sum + frame.size, 0);
|
|
4753
|
+
log.droppedFrames = connection.droppedFrames;
|
|
4754
|
+
if (connection.closeCode != null) log.socketCloseCode = connection.closeCode;
|
|
4755
|
+
}
|
|
4756
|
+
getConnections() {
|
|
4757
|
+
return this.connections.map((connection) => ({
|
|
4758
|
+
...connection,
|
|
4759
|
+
frames: [...connection.frames]
|
|
4760
|
+
}));
|
|
4761
|
+
}
|
|
4762
|
+
clear() {
|
|
4763
|
+
this.connections = [];
|
|
4764
|
+
this.totalBytes = 0;
|
|
4765
|
+
this.logByConnectionId.clear();
|
|
4766
|
+
}
|
|
4767
|
+
/**
|
|
4768
|
+
* 백업에서 되돌린 연결을 지금 목록 앞에 합친다.
|
|
4769
|
+
*
|
|
4770
|
+
* 합친 결과가 상한을 넘으면 앞쪽(오래된 쪽)부터 잘려나간다 — 그중엔 이미
|
|
4771
|
+
* `logByConnectionId` 에 줄이 등록된 연결(리로드 사이 짧게 살아있던 연결)이 섞여 있을
|
|
4772
|
+
* 수 있다. 맵에서도 같이 지우지 않으면 다시 채워질 일 없는 항목으로 계속 남는다.
|
|
4773
|
+
*
|
|
4774
|
+
* 창·예산도 복원 직후 바로 적용한다 — 안 하면 다음 프레임이 올 때까지 상한 없이
|
|
4775
|
+
* 방치된다. 리로드 직후 되돌린 연결이 정확히 이 경우다.
|
|
4776
|
+
*
|
|
4777
|
+
* **`nextId` 도 여기서 올린다.** `nextId` 는 매 세션 1 에서 시작하는데 복원은 그걸 모르고
|
|
4778
|
+
* 지나간다 — MPA 리로드 뒤 백업에서 `ws-1` 이 돌아오고, 그 문서에서 새로 여는 첫 소켓도
|
|
4779
|
+
* 똑같이 `ws-1` 을 받는다. 전사 파일에 같은 id 가 둘 생기고, 뷰어의
|
|
4780
|
+
* `find((c) => c.id === connectionId)` 가 배열 앞쪽(복원된 옛 연결)을 집어 새 연결을 눌러도
|
|
4781
|
+
* 옛 프레임이 보인다. id 에 세션 접두사를 박는 대안도 있지만, 그러면 MCP 조회의
|
|
4782
|
+
* url·direction 필터가 문자열 그대로 비교하는 기존 `ws-<n>` 형식이 깨진다. 복원된 id 의
|
|
4783
|
+
* 최댓값 다음부터 잇는 쪽이 형식을 그대로 지키면서 diff 도 가장 작다.
|
|
4784
|
+
*/
|
|
4785
|
+
restore(connections) {
|
|
4786
|
+
if (connections.length === 0) return;
|
|
4787
|
+
this.connections = [...connections, ...this.connections].slice(-SOCKET_CONNECTION_LIMIT);
|
|
4788
|
+
this.totalBytes = this.sumBytes(this.connections);
|
|
4789
|
+
this.bumpNextIdPast(this.connections);
|
|
4790
|
+
const survivingIds = new Set(this.connections.map((connection) => connection.id));
|
|
4791
|
+
for (const id of this.logByConnectionId.keys()) {
|
|
4792
|
+
if (!survivingIds.has(id)) this.logByConnectionId.delete(id);
|
|
4793
|
+
}
|
|
4794
|
+
this.prune();
|
|
4795
|
+
}
|
|
4796
|
+
/** `ws-<n>` 형식 id 중 가장 큰 번호 다음부터 잇도록 `nextId` 를 올린다. 낮추지는 않는다. */
|
|
4797
|
+
bumpNextIdPast(connections) {
|
|
4798
|
+
for (const connection of connections) {
|
|
4799
|
+
const match2 = /^ws-(\d+)$/.exec(connection.id);
|
|
4800
|
+
if (!match2) continue;
|
|
4801
|
+
const numeric = Number(match2[1]);
|
|
4802
|
+
if (numeric >= this.nextId) this.nextId = numeric + 1;
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
sumBytes(connections) {
|
|
4806
|
+
var _a;
|
|
4807
|
+
let sum = 0;
|
|
4808
|
+
for (const connection of connections) {
|
|
4809
|
+
for (const frame of connection.frames) sum += ((_a = frame.data) == null ? void 0 : _a.length) ?? 0;
|
|
4810
|
+
}
|
|
4811
|
+
return sum;
|
|
4812
|
+
}
|
|
4813
|
+
/**
|
|
4814
|
+
* 우리 백엔드로 가는 연결은 레코드를 아예 만들지 않는다.
|
|
4815
|
+
*
|
|
4816
|
+
* `byInstance` 에 등록되지 않으면 이후 `send` 프레임도 자동으로 따라오지 않는다
|
|
4817
|
+
* (patchedSend 가 `byInstance.get` 실패 시 아무것도 기록하지 않는다).
|
|
4818
|
+
*/
|
|
4819
|
+
track(socket, url, protocols) {
|
|
4820
|
+
var _a;
|
|
4821
|
+
try {
|
|
4822
|
+
if (isSelfTrafficOrigin(toComparableOrigin(url), this.apiBaseUrl ? [this.apiBaseUrl] : []))
|
|
4823
|
+
return null;
|
|
4824
|
+
const connection = {
|
|
4825
|
+
id: `ws-${this.nextId++}`,
|
|
4826
|
+
url: redactUrlSecrets(url),
|
|
4827
|
+
// 서브프로토콜에 토큰을 싣는 패턴(`new WebSocket(url, [token])`)이 실제로 쓰인다.
|
|
4828
|
+
// 값 형태 스크럽까지 거는 이유는 프레임 본문과 같다.
|
|
4829
|
+
protocol: maskFrameText(Array.isArray(protocols) ? protocols[0] : protocols),
|
|
4830
|
+
openedAt: Date.now(),
|
|
4831
|
+
frames: [],
|
|
4832
|
+
droppedFrames: 0,
|
|
4833
|
+
droppedBytes: 0
|
|
4834
|
+
};
|
|
4835
|
+
this.connections.push(connection);
|
|
4836
|
+
if (this.connections.length > SOCKET_CONNECTION_LIMIT) {
|
|
4837
|
+
const closedIndex = this.connections.findIndex((c2) => c2.closedAt != null);
|
|
4838
|
+
const [evicted] = this.connections.splice(closedIndex >= 0 ? closedIndex : 0, 1);
|
|
4839
|
+
this.totalBytes -= this.sumBytes([evicted]);
|
|
4840
|
+
this.logByConnectionId.delete(evicted.id);
|
|
4841
|
+
}
|
|
4842
|
+
socket.addEventListener("message", (event) => {
|
|
4843
|
+
this.addFrame(connection, "recv", event.data);
|
|
4844
|
+
});
|
|
4845
|
+
socket.addEventListener("close", (event) => {
|
|
4846
|
+
var _a2;
|
|
4847
|
+
if (!this.running) return;
|
|
4848
|
+
try {
|
|
4849
|
+
const closeEvent = event;
|
|
4850
|
+
connection.closedAt = Date.now();
|
|
4851
|
+
connection.closeCode = closeEvent.code;
|
|
4852
|
+
connection.closeReason = closeEvent.reason;
|
|
4853
|
+
(_a2 = this.onChange) == null ? void 0 : _a2.call(this);
|
|
4854
|
+
this.syncConnectionLog(connection);
|
|
4855
|
+
} catch {
|
|
4856
|
+
this.recordInstrumentationFailure();
|
|
4857
|
+
}
|
|
4858
|
+
});
|
|
4859
|
+
socket.addEventListener("error", () => {
|
|
4860
|
+
var _a2;
|
|
4861
|
+
if (!this.running) return;
|
|
4862
|
+
try {
|
|
4863
|
+
connection.errored = true;
|
|
4864
|
+
(_a2 = this.onChange) == null ? void 0 : _a2.call(this);
|
|
4865
|
+
} catch {
|
|
4866
|
+
this.recordInstrumentationFailure();
|
|
4867
|
+
}
|
|
4868
|
+
});
|
|
4869
|
+
(_a = this.onChange) == null ? void 0 : _a.call(this);
|
|
4870
|
+
this.emitConnectionLog(connection);
|
|
4871
|
+
return connection;
|
|
4872
|
+
} catch {
|
|
4873
|
+
this.recordInstrumentationFailure();
|
|
4874
|
+
return null;
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
/**
|
|
4878
|
+
* 계측이 스스로 꺼지는 차단기. NetworkCollector.recordInstrumentationFailure 와 같은 규칙이다.
|
|
4879
|
+
*
|
|
4880
|
+
* 계측에서 난 예외가 반복된다는 건 우리가 이해하지 못하는 환경에 들어와 있다는 뜻이다.
|
|
4881
|
+
* 고객 페이지에 계속 손대는 것보다 패치를 걷어내고 로그를 포기하는 쪽이 안전하다.
|
|
4882
|
+
*/
|
|
4883
|
+
recordInstrumentationFailure() {
|
|
4884
|
+
this.instrumentationFailures += 1;
|
|
4885
|
+
if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT) {
|
|
4886
|
+
this.stop();
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
addFrame(connection, dir, data) {
|
|
4890
|
+
var _a;
|
|
4891
|
+
if (!this.running) return;
|
|
4892
|
+
try {
|
|
4893
|
+
const captured = toFrameData(data, dir);
|
|
4894
|
+
const truncated = captured.size > SOCKET_FRAME_MAX_BYTES;
|
|
4895
|
+
const frameData = this.buildFrameData(captured, truncated);
|
|
4896
|
+
connection.frames.push({
|
|
4897
|
+
t: Date.now(),
|
|
4898
|
+
dir,
|
|
4899
|
+
opcode: captured.opcode,
|
|
4900
|
+
size: captured.size,
|
|
4901
|
+
data: frameData,
|
|
4902
|
+
...truncated ? { truncated: true } : {}
|
|
4903
|
+
});
|
|
4904
|
+
this.totalBytes += frameData.length;
|
|
4905
|
+
this.prune();
|
|
4906
|
+
(_a = this.onChange) == null ? void 0 : _a.call(this);
|
|
4907
|
+
this.syncConnectionLog(connection);
|
|
4908
|
+
} catch {
|
|
4909
|
+
this.recordInstrumentationFailure();
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
/**
|
|
4913
|
+
* 프레임 본문을 마스킹·절단해 저장용 문자열로 만든다.
|
|
4914
|
+
*
|
|
4915
|
+
* **상한 아래면 지금까지와 완전히 같다** — 아래 두 분기 모두 `truncated` 가 false 면
|
|
4916
|
+
* 원본 그대로 마스킹만 거치고 절단 코드를 타지 않는다.
|
|
4917
|
+
*
|
|
4918
|
+
* 상한을 넘을 때만 순서가 바뀐다: 절단은 항상 크기(`byteLength`/`Blob.size`/문자열
|
|
4919
|
+
* 길이)를 이미 아는 값으로 먼저 가늠하고, **변환(base64)·마스킹은 그 뒤 필요한 만큼만**
|
|
4920
|
+
* 돌린다. 텍스트는 마스킹이 끝난 뒤 최종 상한으로 한 번 더 자른다 — 마스킹이 앞부분을
|
|
4921
|
+
* 줄이거나 늘릴 수 있어서다(`MASK_PRESLICE_MARGIN_BYTES` 주석 참조).
|
|
4922
|
+
*/
|
|
4923
|
+
buildFrameData(captured, truncated) {
|
|
4924
|
+
if (captured.opcode === "binary") {
|
|
4925
|
+
if (!truncated) return bytesToBase64(captured.bytes);
|
|
4926
|
+
const bytesNeeded = Math.min(
|
|
4927
|
+
captured.bytes.length,
|
|
4928
|
+
bytesNeededForBase64Length(SOCKET_FRAME_MAX_BYTES)
|
|
4929
|
+
);
|
|
4930
|
+
return bytesToBase64(captured.bytes.subarray(0, bytesNeeded)).slice(
|
|
4931
|
+
0,
|
|
4932
|
+
SOCKET_FRAME_MAX_BYTES
|
|
4933
|
+
);
|
|
4934
|
+
}
|
|
4935
|
+
if (!truncated) return maskFrameText(captured.text);
|
|
4936
|
+
const preSliced = captured.text.slice(0, MASK_PRESLICE_MARGIN_BYTES);
|
|
4937
|
+
return maskFrameText(preSliced).slice(0, SOCKET_FRAME_MAX_BYTES);
|
|
4938
|
+
}
|
|
4939
|
+
/**
|
|
4940
|
+
* 시간 창 밖 프레임과 전역 바이트 예산 초과분을 버린다.
|
|
4941
|
+
*
|
|
4942
|
+
* 창 정리는 연결별로 본다 — 각 연결의 프레임은 시간순이라 앞에서부터 잘라내면 된다.
|
|
4943
|
+
* 예산은 전역이라 연결이 여럿이면 가장 오래된 프레임을 가진 연결부터 줄인다.
|
|
4944
|
+
*/
|
|
4945
|
+
prune() {
|
|
4946
|
+
var _a, _b, _c;
|
|
4947
|
+
const cutoff = Date.now() - SOCKET_FRAME_WINDOW_MS;
|
|
4948
|
+
const touched = /* @__PURE__ */ new Set();
|
|
4949
|
+
for (const connection of this.connections) {
|
|
4950
|
+
let removed = 0;
|
|
4951
|
+
let removedBytes = 0;
|
|
4952
|
+
while (connection.frames.length > 0 && connection.frames[0].t < cutoff) {
|
|
4953
|
+
const [frame] = connection.frames.splice(0, 1);
|
|
4954
|
+
removed += 1;
|
|
4955
|
+
removedBytes += ((_a = frame.data) == null ? void 0 : _a.length) ?? 0;
|
|
4956
|
+
}
|
|
4957
|
+
if (removed > 0) {
|
|
4958
|
+
connection.droppedFrames += removed;
|
|
4959
|
+
connection.droppedBytes += removedBytes;
|
|
4960
|
+
this.totalBytes -= removedBytes;
|
|
4961
|
+
touched.add(connection);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
while (this.totalBytes > SOCKET_FRAME_BUDGET_BYTES) {
|
|
4965
|
+
const oldest = this.connections.filter((connection) => connection.frames.length > 0).sort((a2, b) => a2.frames[0].t - b.frames[0].t)[0];
|
|
4966
|
+
if (!oldest) break;
|
|
4967
|
+
const [frame] = oldest.frames.splice(0, 1);
|
|
4968
|
+
oldest.droppedFrames += 1;
|
|
4969
|
+
oldest.droppedBytes += ((_b = frame.data) == null ? void 0 : _b.length) ?? 0;
|
|
4970
|
+
this.totalBytes -= ((_c = frame.data) == null ? void 0 : _c.length) ?? 0;
|
|
4971
|
+
touched.add(oldest);
|
|
4972
|
+
}
|
|
4973
|
+
for (const connection of touched) this.syncConnectionLog(connection);
|
|
4974
|
+
}
|
|
4975
|
+
patch() {
|
|
4976
|
+
this.originalWebSocket = globalThis.WebSocket;
|
|
4977
|
+
const OriginalWebSocket = this.originalWebSocket;
|
|
4978
|
+
const track = this.track.bind(this);
|
|
4979
|
+
const addFrame = this.addFrame.bind(this);
|
|
4980
|
+
const byInstance = /* @__PURE__ */ new WeakMap();
|
|
4981
|
+
class PatchedWebSocket extends OriginalWebSocket {
|
|
4982
|
+
constructor(url, protocols) {
|
|
4983
|
+
super(url, protocols);
|
|
4984
|
+
const connection = track(this, typeof url === "string" ? url : url.href, protocols);
|
|
4985
|
+
if (connection) byInstance.set(this, connection);
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
this.originalSend = OriginalWebSocket.prototype.send;
|
|
4989
|
+
const originalSend = this.originalSend;
|
|
4990
|
+
const patchedSend = function(data) {
|
|
4991
|
+
const connection = byInstance.get(this);
|
|
4992
|
+
if (connection) addFrame(connection, "send", data);
|
|
4993
|
+
return originalSend.call(this, data);
|
|
4994
|
+
};
|
|
4995
|
+
OriginalWebSocket.prototype.send = patchedSend;
|
|
4996
|
+
this.patchedSend = patchedSend;
|
|
4997
|
+
globalThis.WebSocket = PatchedWebSocket;
|
|
4998
|
+
this.patchedWebSocket = PatchedWebSocket;
|
|
4999
|
+
}
|
|
5000
|
+
/**
|
|
5001
|
+
* 우리 래퍼가 아직 현재 값일 때만 되돌린다.
|
|
5002
|
+
*
|
|
5003
|
+
* `NetworkCollector.restoreFetch`/`restoreXHR` 과 같은 규칙 — 무조건 되돌리면 우리
|
|
5004
|
+
* 뒤에 붙은 남의 계측(다른 APM 등)의 래퍼가 통째로 사라진다. 남의 것이 위에 있으면
|
|
5005
|
+
* 우리 래퍼는 체인에 남겨 두고 수집만 멈춘다.
|
|
5006
|
+
*/
|
|
5007
|
+
unpatch() {
|
|
5008
|
+
if (this.originalWebSocket && globalThis.WebSocket === this.patchedWebSocket) {
|
|
5009
|
+
globalThis.WebSocket = this.originalWebSocket;
|
|
5010
|
+
}
|
|
5011
|
+
if (this.originalWebSocket && this.originalSend && this.originalWebSocket.prototype.send === this.patchedSend) {
|
|
5012
|
+
this.originalWebSocket.prototype.send = this.originalSend;
|
|
5013
|
+
}
|
|
5014
|
+
this.originalWebSocket = null;
|
|
5015
|
+
this.patchedWebSocket = null;
|
|
5016
|
+
this.originalSend = null;
|
|
5017
|
+
this.patchedSend = null;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
async function sha256Hex(bytes) {
|
|
5021
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
|
|
5022
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5023
|
+
}
|
|
5024
|
+
async function serializeSocketTranscript(connections) {
|
|
5025
|
+
const frameCount = connections.reduce(
|
|
5026
|
+
(sum, connection) => sum + connection.frames.length + connection.droppedFrames,
|
|
5027
|
+
0
|
|
5028
|
+
);
|
|
5029
|
+
if (connections.length === 0 || frameCount === 0) return null;
|
|
5030
|
+
const json = JSON.stringify({ connections });
|
|
5031
|
+
const payload = gzip_1(json);
|
|
5032
|
+
return {
|
|
5033
|
+
payload,
|
|
5034
|
+
checksumSha256: await sha256Hex(payload),
|
|
5035
|
+
sizeBytes: payload.byteLength,
|
|
5036
|
+
connectionCount: connections.length,
|
|
5037
|
+
frameCount
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
4684
5040
|
var NetworkLogType = /* @__PURE__ */ ((NetworkLogType2) => {
|
|
4685
5041
|
NetworkLogType2["MAIN_FRAME"] = "main_frame";
|
|
4686
5042
|
NetworkLogType2["SUB_FRAME"] = "sub_frame";
|
|
@@ -13299,6 +13655,7 @@ function OnboardingOverlay({
|
|
|
13299
13655
|
targetRect,
|
|
13300
13656
|
targetRadius,
|
|
13301
13657
|
avoidRects,
|
|
13658
|
+
affordanceRect,
|
|
13302
13659
|
elevateHighlight,
|
|
13303
13660
|
allowSkip = true,
|
|
13304
13661
|
className,
|
|
@@ -13318,6 +13675,23 @@ function OnboardingOverlay({
|
|
|
13318
13675
|
cutoutRect.width / 2,
|
|
13319
13676
|
cutoutRect.height / 2
|
|
13320
13677
|
) : 0;
|
|
13678
|
+
const hasAffordanceTarget = !!step.affordanceTargetId;
|
|
13679
|
+
const affordanceCutout = affordanceRect ? padRect(affordanceRect, MASK_PADDING) : null;
|
|
13680
|
+
const affordanceDiameter = affordanceRect ? Math.max(affordanceRect.width, affordanceRect.height) + MASK_PADDING * 2 : 0;
|
|
13681
|
+
const affordanceCircle = affordanceRect ? (() => {
|
|
13682
|
+
const centerX = (affordanceRect.left + affordanceRect.right) / 2;
|
|
13683
|
+
const centerY = (affordanceRect.top + affordanceRect.bottom) / 2;
|
|
13684
|
+
const radius = affordanceDiameter / 2;
|
|
13685
|
+
return {
|
|
13686
|
+
left: centerX - radius,
|
|
13687
|
+
top: centerY - radius,
|
|
13688
|
+
right: centerX + radius,
|
|
13689
|
+
bottom: centerY + radius,
|
|
13690
|
+
width: affordanceDiameter,
|
|
13691
|
+
height: affordanceDiameter
|
|
13692
|
+
};
|
|
13693
|
+
})() : null;
|
|
13694
|
+
const popoverAnchorRect = hasAffordanceTarget && affordanceCutout ? affordanceCutout : cutoutRect;
|
|
13321
13695
|
const popoverRef = React.useRef(null);
|
|
13322
13696
|
const [popoverSize, setPopoverSize] = React.useState(
|
|
13323
13697
|
null
|
|
@@ -13339,17 +13713,20 @@ function OnboardingOverlay({
|
|
|
13339
13713
|
}, [step.id, step.body]);
|
|
13340
13714
|
const effectiveSize = popoverSize && popoverSize.width > 0 && popoverSize.height > 0 ? popoverSize : DEFAULT_POPOVER_SIZE;
|
|
13341
13715
|
const placement = resolvePopoverPlacement(
|
|
13342
|
-
step.position ?? (
|
|
13343
|
-
|
|
13716
|
+
step.position ?? (popoverAnchorRect ? "bottom" : "center"),
|
|
13717
|
+
popoverAnchorRect ?? getViewportRect(),
|
|
13344
13718
|
effectiveSize,
|
|
13345
|
-
|
|
13719
|
+
popoverAnchorRect ? avoidRects ?? [] : []
|
|
13346
13720
|
);
|
|
13347
13721
|
const { left: popoverLeft, top: popoverTop } = placement;
|
|
13348
|
-
const shouldCenterPopover = !
|
|
13722
|
+
const shouldCenterPopover = !popoverAnchorRect && (step.position ?? "center") === "center";
|
|
13349
13723
|
const maskColor = isTargetInteractionStep(step) ? "rgba(8, 13, 18, 0.74)" : "rgba(8, 13, 18, 0.62)";
|
|
13350
13724
|
const isPassthrough = !!step.advanceOn;
|
|
13351
13725
|
const isTargetInteraction = isTargetInteractionStep(step);
|
|
13352
13726
|
const clickHint = isTargetInteraction ? step.clickHint ?? labels.clickHint : null;
|
|
13727
|
+
const isCutoutInteractionStyled = isTargetInteraction && !hasAffordanceTarget;
|
|
13728
|
+
const showAffordanceEmphasis = isTargetInteraction && hasAffordanceTarget && !!affordanceCircle;
|
|
13729
|
+
const ringCursorRect = !isTargetInteraction ? null : hasAffordanceTarget ? affordanceCircle : cutoutRect;
|
|
13353
13730
|
return /* @__PURE__ */ jsxs(
|
|
13354
13731
|
"div",
|
|
13355
13732
|
{
|
|
@@ -13442,7 +13819,7 @@ function OnboardingOverlay({
|
|
|
13442
13819
|
"data-testid": "onboarding-cutout-outline",
|
|
13443
13820
|
className: cn(
|
|
13444
13821
|
"issue-sticker-onboarding-highlight fixed",
|
|
13445
|
-
|
|
13822
|
+
isCutoutInteractionStyled ? "border-2 border-brand" : "border border-white/90"
|
|
13446
13823
|
),
|
|
13447
13824
|
style: {
|
|
13448
13825
|
left: cutoutRect.left,
|
|
@@ -13457,7 +13834,31 @@ function OnboardingOverlay({
|
|
|
13457
13834
|
step.id
|
|
13458
13835
|
)
|
|
13459
13836
|
) : null,
|
|
13460
|
-
|
|
13837
|
+
showAffordanceEmphasis && affordanceCircle ? renderHighlight(
|
|
13838
|
+
/* 강조(컷아웃)와 클릭 유도 위치가 분리된 단계의 브랜드 링. 컷아웃 테두리는
|
|
13839
|
+
* 위에서 이미 순수 강조 스타일로 그렸으니, 여기서는 실제로 눌러야 할 요소
|
|
13840
|
+
* 중심에 원을 그린다 — 사각형 컷아웃과 구별돼야 "강조 대상이 아니라 이걸
|
|
13841
|
+
* 누르라"는 뜻이 선다. */
|
|
13842
|
+
/* @__PURE__ */ jsx(
|
|
13843
|
+
"div",
|
|
13844
|
+
{
|
|
13845
|
+
"aria-hidden": "true",
|
|
13846
|
+
"data-testid": "onboarding-affordance-ring",
|
|
13847
|
+
className: "issue-sticker-onboarding-highlight fixed rounded-full border-2 border-brand",
|
|
13848
|
+
style: {
|
|
13849
|
+
left: affordanceCircle.left,
|
|
13850
|
+
top: affordanceCircle.top,
|
|
13851
|
+
width: affordanceCircle.width,
|
|
13852
|
+
height: affordanceCircle.height,
|
|
13853
|
+
borderRadius: 9999,
|
|
13854
|
+
pointerEvents: "none",
|
|
13855
|
+
zIndex: elevateHighlight ? OVERLAY_LAYER_INDEX.runtimeHost : void 0
|
|
13856
|
+
}
|
|
13857
|
+
},
|
|
13858
|
+
`${step.id}-affordance`
|
|
13859
|
+
)
|
|
13860
|
+
) : null,
|
|
13861
|
+
ringCursorRect ? renderHighlight(
|
|
13461
13862
|
/* 클릭하는 커서. 테두리만으로는 "봐야 할 곳"까지만 전달되고 "클릭하라"가
|
|
13462
13863
|
* 전달되지 않는다. 손 모양 반복 동작이 그 자리를 맡는다. */
|
|
13463
13864
|
/* @__PURE__ */ jsxs(
|
|
@@ -13467,8 +13868,8 @@ function OnboardingOverlay({
|
|
|
13467
13868
|
"data-testid": "onboarding-cutout-cursor",
|
|
13468
13869
|
className: "issue-sticker-onboarding-tap fixed",
|
|
13469
13870
|
style: {
|
|
13470
|
-
left:
|
|
13471
|
-
top:
|
|
13871
|
+
left: ringCursorRect.right - TAP_INSET,
|
|
13872
|
+
top: ringCursorRect.bottom - TAP_INSET,
|
|
13472
13873
|
pointerEvents: "none",
|
|
13473
13874
|
zIndex: elevateHighlight ? OVERLAY_LAYER_INDEX.runtimeHost : void 0
|
|
13474
13875
|
},
|
|
@@ -13705,18 +14106,24 @@ function OnboardingProvider({
|
|
|
13705
14106
|
() => activeTargetIds.map((id) => targetMapRef.current.get(id) ?? null).filter((el) => el !== null),
|
|
13706
14107
|
[activeTargetIds]
|
|
13707
14108
|
);
|
|
14109
|
+
const activeAffordanceTargetId = activeStep == null ? void 0 : activeStep.affordanceTargetId;
|
|
14110
|
+
const getObservedElements = React.useCallback(() => {
|
|
14111
|
+
const elements = getActiveTargetElements();
|
|
14112
|
+
const affordanceElement = activeAffordanceTargetId ? targetMapRef.current.get(activeAffordanceTargetId) ?? null : null;
|
|
14113
|
+
return affordanceElement ? [...elements, affordanceElement] : elements;
|
|
14114
|
+
}, [activeAffordanceTargetId, getActiveTargetElements]);
|
|
13708
14115
|
const scheduleLayoutUpdate = React.useCallback(() => {
|
|
13709
14116
|
clearLayoutFrame();
|
|
13710
14117
|
frameRef.current = window.requestAnimationFrame(() => {
|
|
13711
14118
|
frameRef.current = null;
|
|
13712
14119
|
const nextRect = unionRect(
|
|
13713
|
-
|
|
14120
|
+
getObservedElements().map(getElementRect).filter((r2) => r2 !== null)
|
|
13714
14121
|
);
|
|
13715
14122
|
if (areRectsEqual(lastRectRef.current, nextRect)) return;
|
|
13716
14123
|
lastRectRef.current = nextRect;
|
|
13717
14124
|
setLayoutVersion((version2) => version2 + 1);
|
|
13718
14125
|
});
|
|
13719
|
-
}, [clearLayoutFrame,
|
|
14126
|
+
}, [clearLayoutFrame, getObservedElements]);
|
|
13720
14127
|
const openAtStep = React.useCallback(
|
|
13721
14128
|
(stepIndex) => {
|
|
13722
14129
|
var _a;
|
|
@@ -13874,7 +14281,7 @@ function OnboardingProvider({
|
|
|
13874
14281
|
}, [activeStep, advanceFromTargetClick, goToStep, isActiveRoute, isOpen]);
|
|
13875
14282
|
React.useEffect(() => {
|
|
13876
14283
|
if (!isOpen || !isActiveRoute) return;
|
|
13877
|
-
const elements =
|
|
14284
|
+
const elements = getObservedElements();
|
|
13878
14285
|
if (elements.length === 0) return;
|
|
13879
14286
|
scheduleLayoutUpdate();
|
|
13880
14287
|
window.addEventListener("resize", scheduleLayoutUpdate);
|
|
@@ -13889,7 +14296,7 @@ function OnboardingProvider({
|
|
|
13889
14296
|
};
|
|
13890
14297
|
}, [
|
|
13891
14298
|
clearLayoutFrame,
|
|
13892
|
-
|
|
14299
|
+
getObservedElements,
|
|
13893
14300
|
isActiveRoute,
|
|
13894
14301
|
isOpen,
|
|
13895
14302
|
scheduleLayoutUpdate,
|
|
@@ -13902,6 +14309,7 @@ function OnboardingProvider({
|
|
|
13902
14309
|
const targetRect = isOpen && isActiveRoute && allTargetsResolved ? unionRect(resolvedTargetRects.filter((r2) => r2 !== null)) : null;
|
|
13903
14310
|
const targetRadius = activeTargetIds.length === 1 ? getElementRadius(targetMapRef.current.get(activeTargetIds[0]) ?? null) : null;
|
|
13904
14311
|
const avoidRects = ((activeStep == null ? void 0 : activeStep.avoidTargetIds) ?? []).map((id) => getElementRect(targetMapRef.current.get(id) ?? null)).filter((rect) => rect !== null);
|
|
14312
|
+
const affordanceRect = (activeStep == null ? void 0 : activeStep.affordanceTargetId) ? getElementRect(targetMapRef.current.get(activeStep.affordanceTargetId) ?? null) : null;
|
|
13905
14313
|
const shouldRenderOverlay = isOpen && activeStep && isActiveRoute && !suppressed && !activeStep.silent && (!stepHasTarget || targetRect);
|
|
13906
14314
|
const isTargetReady = !stepHasTarget || targetRect !== null;
|
|
13907
14315
|
React.useEffect(() => {
|
|
@@ -13954,6 +14362,7 @@ function OnboardingProvider({
|
|
|
13954
14362
|
targetRect,
|
|
13955
14363
|
targetRadius,
|
|
13956
14364
|
avoidRects,
|
|
14365
|
+
affordanceRect,
|
|
13957
14366
|
elevateHighlight,
|
|
13958
14367
|
allowSkip,
|
|
13959
14368
|
className,
|
|
@@ -14175,6 +14584,20 @@ function filterExcludeServiceRelatedConsoleLogs(consoleLogs) {
|
|
|
14175
14584
|
return !isIssueStickerInternalConsoleLog(log) && !isIssueStickerServiceConsoleLog(log);
|
|
14176
14585
|
});
|
|
14177
14586
|
}
|
|
14587
|
+
const TRANSCRIPT_CONTENT_TYPE = "application/gzip";
|
|
14588
|
+
async function uploadSocketTranscript(api, snapshot, options = {}) {
|
|
14589
|
+
if (snapshot.sizeBytes > SOCKET_TRANSCRIPT_MAX_BYTES) return null;
|
|
14590
|
+
try {
|
|
14591
|
+
const file = new File([snapshot.payload], "socket-transcript.json.gz", {
|
|
14592
|
+
type: TRANSCRIPT_CONTENT_TYPE
|
|
14593
|
+
});
|
|
14594
|
+
const uploaded = await uploadSignedMediaWithApi(api, file, "socket", options);
|
|
14595
|
+
return { uploadId: uploaded.id };
|
|
14596
|
+
} catch (error) {
|
|
14597
|
+
devLogger.error("socketTranscriptUpload", "소켓 전사 업로드 실패:", error);
|
|
14598
|
+
return null;
|
|
14599
|
+
}
|
|
14600
|
+
}
|
|
14178
14601
|
function buildExternalUserPayload(user) {
|
|
14179
14602
|
if (!user) return {};
|
|
14180
14603
|
return {
|
|
@@ -14183,60 +14606,63 @@ function buildExternalUserPayload(user) {
|
|
|
14183
14606
|
};
|
|
14184
14607
|
}
|
|
14185
14608
|
export {
|
|
14186
|
-
|
|
14187
|
-
|
|
14188
|
-
|
|
14609
|
+
Check as $,
|
|
14610
|
+
trimLogBackupPayload as A,
|
|
14611
|
+
createSafeLogBackup as B,
|
|
14189
14612
|
ConsoleCollector as C,
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14613
|
+
getLogBackupSessionId as D,
|
|
14614
|
+
createIndexedDbLogBackup as E,
|
|
14615
|
+
PopoverTrigger as F,
|
|
14616
|
+
Input as G,
|
|
14617
|
+
PopoverContent as H,
|
|
14195
14618
|
ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
|
|
14196
|
-
|
|
14197
|
-
|
|
14619
|
+
Select$1 as J,
|
|
14620
|
+
SelectTrigger as K,
|
|
14198
14621
|
Label as L,
|
|
14199
|
-
|
|
14622
|
+
SelectValue as M,
|
|
14200
14623
|
NetworkCollector as N,
|
|
14201
|
-
|
|
14624
|
+
SelectContent as O,
|
|
14202
14625
|
Popover as P,
|
|
14203
|
-
|
|
14204
|
-
|
|
14626
|
+
SelectItem as Q,
|
|
14627
|
+
getJiraMetadata as R,
|
|
14205
14628
|
ScreenRecordingController as S,
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14629
|
+
buildJiraIssueFields as T,
|
|
14630
|
+
Button$1 as U,
|
|
14631
|
+
Root2 as V,
|
|
14632
|
+
WebSocketCollector as W,
|
|
14633
|
+
Trigger as X,
|
|
14634
|
+
ChevronDown as Y,
|
|
14635
|
+
Portal as Z,
|
|
14636
|
+
Content2 as _,
|
|
14214
14637
|
uploadSignedMediaWithApi as a,
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14638
|
+
X as a0,
|
|
14639
|
+
DatePicker as a1,
|
|
14640
|
+
getNotionPageMetadata as a2,
|
|
14641
|
+
buildNotionFieldViews as a3,
|
|
14642
|
+
DirectUploadError as a4,
|
|
14643
|
+
toast as a5,
|
|
14644
|
+
getSnapshot as a6,
|
|
14645
|
+
subscribe as a7,
|
|
14646
|
+
getRegisteredUploadId as a8,
|
|
14647
|
+
ChevronRight as a9,
|
|
14648
|
+
ReactRemoveScroll as aa,
|
|
14649
|
+
hideOthers as ab,
|
|
14650
|
+
useFocusGuards as ac,
|
|
14651
|
+
FocusScope as ad,
|
|
14652
|
+
getExposedJiraFields as ae,
|
|
14653
|
+
isJiraFieldExposed as af,
|
|
14654
|
+
normalizeExposedJiraFields as ag,
|
|
14655
|
+
getPrimaryJiraMapping as ah,
|
|
14656
|
+
createCollection as ai,
|
|
14657
|
+
useDirection as aj,
|
|
14658
|
+
getPrimaryNotionMapping as ak,
|
|
14659
|
+
buttonVariants as al,
|
|
14660
|
+
findElementBySelector as am,
|
|
14661
|
+
findElementByXPath as an,
|
|
14662
|
+
OnboardingProvider as ao,
|
|
14663
|
+
useOnboardingTarget as ap,
|
|
14664
|
+
useOnboardingFlow as aq,
|
|
14665
|
+
PopoverAnchor as ar,
|
|
14240
14666
|
buildJiraCreateFields as b,
|
|
14241
14667
|
captureWithModernScreenshot as c,
|
|
14242
14668
|
directUploadToSignedUrl as d,
|
|
@@ -14257,9 +14683,9 @@ export {
|
|
|
14257
14683
|
selectNetworkLogsForSubmit as s,
|
|
14258
14684
|
sanitizeString as t,
|
|
14259
14685
|
uploadScreenshotDataUrls as u,
|
|
14260
|
-
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14686
|
+
serializeSocketTranscript as v,
|
|
14687
|
+
uploadSocketTranscript as w,
|
|
14688
|
+
getCurrentPageUrl as x,
|
|
14689
|
+
SessionReplayCaptureController as y,
|
|
14690
|
+
preloadModernScreenshot as z
|
|
14265
14691
|
};
|