issue-sticker 0.8.1 → 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 +21 -0
- package/README.md +33 -0
- package/dist/{externalUserPayload-D-P1-QaW.js → externalUserPayload-CVp5Hz2L.js} +956 -585
- 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";
|
|
@@ -207,98 +207,361 @@ class ScreenRecordingController {
|
|
|
207
207
|
this.listeners.forEach((listener) => listener(this.snapshot));
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
"[data-cs-mask]",
|
|
253
|
-
"[data-cs-encrypt]",
|
|
254
|
-
// Heap
|
|
255
|
-
"[data-heap-redact-text]",
|
|
256
|
-
"[data-heap-redact-attributes]",
|
|
257
|
-
// Amplitude
|
|
258
|
-
"[data-amp-mask]",
|
|
259
|
-
// Matomo
|
|
260
|
-
"[data-matomo-mask]"
|
|
261
|
-
];
|
|
262
|
-
const FOREIGN_UNMASK_SELECTORS = [
|
|
263
|
-
".fs-unmask",
|
|
264
|
-
".sentry-unmask",
|
|
265
|
-
"[data-clarity-unmask]",
|
|
266
|
-
"[data-amp-unmask]",
|
|
267
|
-
"[data-cs-capture]"
|
|
268
|
-
];
|
|
269
|
-
const SESSION_REPLAY_FULL_MASK_SELECTOR = [
|
|
270
|
-
`.${SESSION_REPLAY_MASK_CLASS}`,
|
|
271
|
-
...FOREIGN_MASK_SELECTORS
|
|
272
|
-
].join(",");
|
|
273
|
-
const SESSION_REPLAY_BLOCK_SELECTOR = [
|
|
274
|
-
`.${SESSION_REPLAY_BLOCK_CLASS}`,
|
|
275
|
-
...FOREIGN_BLOCK_SELECTORS
|
|
276
|
-
].join(",");
|
|
277
|
-
const SESSION_REPLAY_UNMASK_SELECTOR = [
|
|
278
|
-
`.${SESSION_REPLAY_UNMASK_CLASS}`,
|
|
279
|
-
...FOREIGN_UNMASK_SELECTORS
|
|
280
|
-
].join(",");
|
|
281
|
-
function isElementFullyMasked(element, maskSelector = SESSION_REPLAY_FULL_MASK_SELECTOR, unmaskSelector = SESSION_REPLAY_UNMASK_SELECTOR) {
|
|
282
|
-
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
|
+
}
|
|
283
252
|
try {
|
|
284
|
-
const
|
|
285
|
-
|
|
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;
|
|
286
263
|
} catch {
|
|
287
|
-
return
|
|
264
|
+
return null;
|
|
288
265
|
}
|
|
289
266
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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 });
|
|
302
565
|
}
|
|
303
566
|
function createSegments() {
|
|
304
567
|
return [{ events: [] }];
|
|
@@ -331,264 +594,49 @@ function trimSegments(segments, windowMs, now, checkoutMs) {
|
|
|
331
594
|
}
|
|
332
595
|
return segments.slice(startIdx);
|
|
333
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
|
+
}
|
|
334
605
|
function flattenEvents(segments) {
|
|
335
606
|
return segments.flatMap((s2) => s2.events);
|
|
336
607
|
}
|
|
337
608
|
function dropEventsAfter(segments, cutoff) {
|
|
338
609
|
return segments.map((s2) => ({ events: s2.events.filter((e2) => e2.timestamp <= cutoff) })).filter((s2) => s2.events.length > 0);
|
|
339
610
|
}
|
|
340
|
-
|
|
611
|
+
const REPLAY_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
612
|
+
async function sha256Hex$1(bytes) {
|
|
341
613
|
const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
|
|
342
614
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
343
615
|
}
|
|
344
616
|
async function serializeSnapshot(input) {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
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);
|
|
349
630
|
return {
|
|
350
631
|
payload,
|
|
351
632
|
checksumSha256,
|
|
352
|
-
windowSeconds:
|
|
633
|
+
windowSeconds: spanSeconds(segments),
|
|
353
634
|
eventCount: events.length,
|
|
354
635
|
sizeBytes: payload.byteLength,
|
|
355
636
|
rrwebVersion: input.rrwebVersion,
|
|
356
637
|
privacyConfigVersion: input.privacyConfigVersion
|
|
357
638
|
};
|
|
358
639
|
}
|
|
359
|
-
const REDACTED_HEADER_VALUE = "[redacted]";
|
|
360
|
-
const SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
361
|
-
"authorization",
|
|
362
|
-
"proxy-authorization",
|
|
363
|
-
"cookie",
|
|
364
|
-
"set-cookie",
|
|
365
|
-
"x-api-key",
|
|
366
|
-
"api-key",
|
|
367
|
-
"apikey",
|
|
368
|
-
"x-auth-token",
|
|
369
|
-
"x-access-token",
|
|
370
|
-
"x-refresh-token",
|
|
371
|
-
"x-session-token",
|
|
372
|
-
"x-csrf-token",
|
|
373
|
-
"x-xsrf-token",
|
|
374
|
-
"x-amz-security-token",
|
|
375
|
-
"x-goog-api-key"
|
|
376
|
-
]);
|
|
377
|
-
function isSensitiveHeaderName(headerName) {
|
|
378
|
-
return SENSITIVE_HEADER_NAMES.has(headerName.toLowerCase());
|
|
379
|
-
}
|
|
380
|
-
function redactSensitiveHeaders(headers) {
|
|
381
|
-
if (!headers) return headers;
|
|
382
|
-
let changed = false;
|
|
383
|
-
const redacted = {};
|
|
384
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
385
|
-
if (value && isSensitiveHeaderName(name)) {
|
|
386
|
-
redacted[name] = REDACTED_HEADER_VALUE;
|
|
387
|
-
changed = true;
|
|
388
|
-
} else {
|
|
389
|
-
redacted[name] = value;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return changed ? redacted : headers;
|
|
393
|
-
}
|
|
394
|
-
const REDACTED_BODY_VALUE = "[redacted]";
|
|
395
|
-
const SENSITIVE_KEY_MARKERS = [
|
|
396
|
-
"password",
|
|
397
|
-
"passwd",
|
|
398
|
-
"pwd",
|
|
399
|
-
"secret",
|
|
400
|
-
"token",
|
|
401
|
-
"apikey",
|
|
402
|
-
"privatekey",
|
|
403
|
-
"credential",
|
|
404
|
-
// 결제·본인확인 계열. 토스페이먼츠 공식 문서의 필드명을 그대로 딴다.
|
|
405
|
-
// cardNumber / customerIdentityNumber / businessNumber / accountNumber(가상계좌)
|
|
406
|
-
"cardnumber",
|
|
407
|
-
"identitynumber",
|
|
408
|
-
"businessnumber",
|
|
409
|
-
"registrationnumber",
|
|
410
|
-
"accountnumber"
|
|
411
|
-
];
|
|
412
|
-
const SENSITIVE_KEY_EXACT = /* @__PURE__ */ new Set([
|
|
413
|
-
"otp",
|
|
414
|
-
"auth",
|
|
415
|
-
"authorization",
|
|
416
|
-
"bearer",
|
|
417
|
-
// 토스페이먼츠. billingKey + customerKey 조합이면 결제를 낼 수 있고, authKey 는 일회성
|
|
418
|
-
// 인증 키다(우리 admin 결제 성공 URL 의 쿼리 파라미터로 실제로 실린다).
|
|
419
|
-
// paymentKey 는 넣지 않는다 — 시크릿 키 없이는 아무것도 못 하는 식별자이고, 결제 장애
|
|
420
|
-
// 진단의 출발점이라 가리면 로그가 쓸모를 잃는다.
|
|
421
|
-
"billingkey",
|
|
422
|
-
"customerkey",
|
|
423
|
-
"authkey"
|
|
424
|
-
]);
|
|
425
|
-
const MAX_WALK_DEPTH = 20;
|
|
426
|
-
function isSensitiveFieldName(name) {
|
|
427
|
-
const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
|
|
428
|
-
if (SENSITIVE_KEY_EXACT.has(normalized)) {
|
|
429
|
-
return true;
|
|
430
|
-
}
|
|
431
|
-
return SENSITIVE_KEY_MARKERS.some((marker) => normalized.includes(marker));
|
|
432
|
-
}
|
|
433
|
-
function redactJsonValue(value, depth) {
|
|
434
|
-
if (typeof value === "string") {
|
|
435
|
-
const redacted = redactSensitiveValues(value);
|
|
436
|
-
return { value: redacted, changed: redacted !== value };
|
|
437
|
-
}
|
|
438
|
-
if (depth > MAX_WALK_DEPTH || value === null || typeof value !== "object") {
|
|
439
|
-
return { value, changed: false };
|
|
440
|
-
}
|
|
441
|
-
if (Array.isArray(value)) {
|
|
442
|
-
let changed2 = false;
|
|
443
|
-
const next2 = value.map((item) => {
|
|
444
|
-
const result = redactJsonValue(item, depth + 1);
|
|
445
|
-
changed2 || (changed2 = result.changed);
|
|
446
|
-
return result.value;
|
|
447
|
-
});
|
|
448
|
-
return changed2 ? { value: next2, changed: changed2 } : { value, changed: changed2 };
|
|
449
|
-
}
|
|
450
|
-
let changed = false;
|
|
451
|
-
const next = {};
|
|
452
|
-
for (const [key, item] of Object.entries(value)) {
|
|
453
|
-
if (item !== "" && item != null && typeof item !== "boolean" && isSensitiveFieldName(key)) {
|
|
454
|
-
next[key] = REDACTED_BODY_VALUE;
|
|
455
|
-
changed = true;
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
const result = redactJsonValue(item, depth + 1);
|
|
459
|
-
changed || (changed = result.changed);
|
|
460
|
-
next[key] = result.value;
|
|
461
|
-
}
|
|
462
|
-
return changed ? { value: next, changed } : { value, changed };
|
|
463
|
-
}
|
|
464
|
-
function redactKeyValuePairs(body) {
|
|
465
|
-
let changed = false;
|
|
466
|
-
const redacted = body.split("&").map((pair) => {
|
|
467
|
-
const separator = pair.indexOf("=");
|
|
468
|
-
if (separator <= 0) {
|
|
469
|
-
return pair;
|
|
470
|
-
}
|
|
471
|
-
const rawKey = pair.slice(0, separator);
|
|
472
|
-
const rawValue = pair.slice(separator + 1);
|
|
473
|
-
if (!rawValue) {
|
|
474
|
-
return pair;
|
|
475
|
-
}
|
|
476
|
-
if (isSensitiveFieldName(decodeURIComponent(rawKey))) {
|
|
477
|
-
changed = true;
|
|
478
|
-
return `${rawKey}=${encodeURIComponent(REDACTED_BODY_VALUE)}`;
|
|
479
|
-
}
|
|
480
|
-
const redactedValue = redactSensitiveValues(decodeURIComponent(rawValue));
|
|
481
|
-
if (redactedValue === decodeURIComponent(rawValue)) {
|
|
482
|
-
return pair;
|
|
483
|
-
}
|
|
484
|
-
changed = true;
|
|
485
|
-
return `${rawKey}=${encodeURIComponent(redactedValue)}`;
|
|
486
|
-
}).join("&");
|
|
487
|
-
return changed ? redacted : body;
|
|
488
|
-
}
|
|
489
|
-
function looksLikeFormUrlEncoded(body) {
|
|
490
|
-
return /^[^=&\s]+=[^&]*(&[^=&\s]+=[^&]*)*$/.test(body);
|
|
491
|
-
}
|
|
492
|
-
function redactSensitiveBody(body) {
|
|
493
|
-
if (!body) return body;
|
|
494
|
-
const trimmed = body.trim();
|
|
495
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
496
|
-
try {
|
|
497
|
-
const parsed = JSON.parse(trimmed);
|
|
498
|
-
const { value, changed } = redactJsonValue(parsed, 0);
|
|
499
|
-
return changed ? JSON.stringify(value) : body;
|
|
500
|
-
} catch {
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
if (looksLikeFormUrlEncoded(trimmed)) {
|
|
504
|
-
const redacted = redactKeyValuePairs(trimmed);
|
|
505
|
-
return redacted === trimmed ? body : redacted;
|
|
506
|
-
}
|
|
507
|
-
return body;
|
|
508
|
-
}
|
|
509
|
-
const JWT_PATTERN = /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g;
|
|
510
|
-
const BEARER_PATTERN = /(Bearer\s+)[A-Za-z0-9._~+/-]{12,}=*/gi;
|
|
511
|
-
const CARD_CANDIDATE_PATTERN = /\b[2-6](?:[ -]?\d){13,18}\b/g;
|
|
512
|
-
const RRN_CANDIDATE_PATTERN = /\b(\d{2})(\d{2})(\d{2})-?([1-8])(\d{6})\b/g;
|
|
513
|
-
const BRN_CANDIDATE_PATTERN = /\b(\d{3})-?(\d{2})-?(\d{5})\b/g;
|
|
514
|
-
const BRN_WEIGHTS = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
|
515
|
-
const KR_MOBILE_PATTERN = /\b01[016789]-?\d{3,4}-?\d{4}\b/g;
|
|
516
|
-
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g;
|
|
517
|
-
function redactEmails(text) {
|
|
518
|
-
if (!text) return text;
|
|
519
|
-
const out = text.replace(EMAIL_PATTERN, REDACTED_BODY_VALUE);
|
|
520
|
-
return out === text ? text : out;
|
|
521
|
-
}
|
|
522
|
-
function passesBrnChecksum(digits) {
|
|
523
|
-
let sum = 0;
|
|
524
|
-
for (let i2 = 0; i2 < 9; i2 += 1) {
|
|
525
|
-
sum += (digits.charCodeAt(i2) - 48) * BRN_WEIGHTS[i2];
|
|
526
|
-
}
|
|
527
|
-
sum += Math.floor((digits.charCodeAt(8) - 48) * 5 / 10);
|
|
528
|
-
return (10 - sum % 10) % 10 === digits.charCodeAt(9) - 48;
|
|
529
|
-
}
|
|
530
|
-
function passesLuhn(digits) {
|
|
531
|
-
let sum = 0;
|
|
532
|
-
let double = false;
|
|
533
|
-
for (let i2 = digits.length - 1; i2 >= 0; i2 -= 1) {
|
|
534
|
-
let digit = digits.charCodeAt(i2) - 48;
|
|
535
|
-
if (double) {
|
|
536
|
-
digit *= 2;
|
|
537
|
-
if (digit > 9) digit -= 9;
|
|
538
|
-
}
|
|
539
|
-
sum += digit;
|
|
540
|
-
double = !double;
|
|
541
|
-
}
|
|
542
|
-
return sum % 10 === 0;
|
|
543
|
-
}
|
|
544
|
-
function looksLikeBirthDate(month, day) {
|
|
545
|
-
const monthValue = Number(month);
|
|
546
|
-
const dayValue = Number(day);
|
|
547
|
-
return monthValue >= 1 && monthValue <= 12 && dayValue >= 1 && dayValue <= 31;
|
|
548
|
-
}
|
|
549
|
-
function redactSensitiveValues(text) {
|
|
550
|
-
if (!text) return text;
|
|
551
|
-
let out = text;
|
|
552
|
-
out = out.replace(JWT_PATTERN, REDACTED_BODY_VALUE);
|
|
553
|
-
out = out.replace(BEARER_PATTERN, `$1${REDACTED_BODY_VALUE}`);
|
|
554
|
-
out = out.replace(CARD_CANDIDATE_PATTERN, (match2) => {
|
|
555
|
-
const digits = match2.replace(/[ -]/g, "");
|
|
556
|
-
return digits.length >= 14 && digits.length <= 19 && passesLuhn(digits) ? REDACTED_BODY_VALUE : match2;
|
|
557
|
-
});
|
|
558
|
-
out = out.replace(
|
|
559
|
-
RRN_CANDIDATE_PATTERN,
|
|
560
|
-
(match2, _year, month, day) => looksLikeBirthDate(month, day) ? REDACTED_BODY_VALUE : match2
|
|
561
|
-
);
|
|
562
|
-
out = out.replace(BRN_CANDIDATE_PATTERN, (match2) => {
|
|
563
|
-
const digits = match2.replace(/-/g, "");
|
|
564
|
-
return passesBrnChecksum(digits) ? REDACTED_BODY_VALUE : match2;
|
|
565
|
-
});
|
|
566
|
-
out = out.replace(KR_MOBILE_PATTERN, REDACTED_BODY_VALUE);
|
|
567
|
-
return out === text ? text : out;
|
|
568
|
-
}
|
|
569
|
-
function redactUrlSecrets(url) {
|
|
570
|
-
if (!url) return url;
|
|
571
|
-
const queryStart = url.indexOf("?");
|
|
572
|
-
const fragmentStart = url.indexOf("#");
|
|
573
|
-
if (queryStart < 0 && fragmentStart < 0) {
|
|
574
|
-
return redactSensitiveValues(url);
|
|
575
|
-
}
|
|
576
|
-
const hasQuery = queryStart >= 0 && (fragmentStart < 0 || queryStart < fragmentStart);
|
|
577
|
-
const base = url.slice(0, hasQuery ? queryStart : fragmentStart);
|
|
578
|
-
const query = hasQuery ? url.slice(queryStart + 1, fragmentStart >= 0 ? fragmentStart : void 0) : "";
|
|
579
|
-
const fragment = fragmentStart >= 0 ? url.slice(fragmentStart + 1) : "";
|
|
580
|
-
const redactedQuery = redactKeyValuePairs(query);
|
|
581
|
-
const redactedFragment = fragment.includes("=") ? redactKeyValuePairs(fragment) : fragment;
|
|
582
|
-
const redactedBase = redactSensitiveValues(base);
|
|
583
|
-
if (redactedBase === base && redactedQuery === query && redactedFragment === fragment) {
|
|
584
|
-
return url;
|
|
585
|
-
}
|
|
586
|
-
return redactedBase + (hasQuery ? `?${redactedQuery}` : "") + (fragmentStart >= 0 ? `#${redactedFragment}` : "");
|
|
587
|
-
}
|
|
588
|
-
function redactSensitiveText(text) {
|
|
589
|
-
if (!text) return text;
|
|
590
|
-
return redactSensitiveValues(redactUrlSecrets(redactSensitiveBody(text)));
|
|
591
|
-
}
|
|
592
640
|
const META_EVENT_TYPE = 4;
|
|
593
641
|
const FULL_SNAPSHOT_EVENT_TYPE = 2;
|
|
594
642
|
const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
|
|
@@ -669,7 +717,13 @@ const RRWEB_VERSION = "2.1.0";
|
|
|
669
717
|
const SESSION_REPLAY_WINDOW_MS = 12e4;
|
|
670
718
|
const SESSION_REPLAY_CHECKOUT_MS = 6e4;
|
|
671
719
|
const REPLAY_MASK_VALUE = "[redacted]";
|
|
672
|
-
|
|
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
|
+
}
|
|
673
727
|
return text ? REPLAY_MASK_VALUE : text;
|
|
674
728
|
}
|
|
675
729
|
function createMaskTextFn(fullMaskSelector, unmaskSelector) {
|
|
@@ -685,19 +739,12 @@ function createMaskTextFn(fullMaskSelector, unmaskSelector) {
|
|
|
685
739
|
};
|
|
686
740
|
}
|
|
687
741
|
const IDLE_CHECKOUT_POLL_MS = 5e3;
|
|
688
|
-
function spanSeconds(segments) {
|
|
689
|
-
var _a, _b;
|
|
690
|
-
const events = flattenEvents(segments);
|
|
691
|
-
const first = (_a = events[0]) == null ? void 0 : _a.timestamp;
|
|
692
|
-
const last = (_b = events[events.length - 1]) == null ? void 0 : _b.timestamp;
|
|
693
|
-
if (first === void 0 || last === void 0) return 0;
|
|
694
|
-
return Math.round((last - first) / 1e3);
|
|
695
|
-
}
|
|
696
742
|
class RingBufferRecorder {
|
|
697
743
|
constructor(options = {}) {
|
|
698
744
|
this.options = options;
|
|
699
745
|
this.segments = createSegments();
|
|
700
746
|
this.stopFn = null;
|
|
747
|
+
this.stopCanvasTracking = null;
|
|
701
748
|
this.idleCheckoutTimer = null;
|
|
702
749
|
this.visibilityListener = null;
|
|
703
750
|
this.tailCutoff = null;
|
|
@@ -748,10 +795,13 @@ class RingBufferRecorder {
|
|
|
748
795
|
this.segments = createSegments();
|
|
749
796
|
const stop = recordFn({
|
|
750
797
|
emit: (event, isCheckout) => {
|
|
751
|
-
var _a, _b;
|
|
798
|
+
var _a, _b, _c;
|
|
752
799
|
this.segments = appendEvent(this.segments, event, Boolean(isCheckout));
|
|
753
800
|
this.segments = trimSegments(this.segments, this.windowMs, now(), this.checkoutEveryMs);
|
|
754
|
-
|
|
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);
|
|
755
805
|
},
|
|
756
806
|
checkoutEveryNms: this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS,
|
|
757
807
|
maskAllInputs: cfg.maskAllInputs,
|
|
@@ -763,6 +813,9 @@ class RingBufferRecorder {
|
|
|
763
813
|
// rrweb 옵션 타입이 버전별로 달라 캐스팅한다.
|
|
764
814
|
});
|
|
765
815
|
this.stopFn = typeof stop === "function" ? stop : null;
|
|
816
|
+
if (this.options.trackCanvasInteractions !== false) {
|
|
817
|
+
this.stopCanvasTracking = startCanvasInteractionTracking();
|
|
818
|
+
}
|
|
766
819
|
this.idleCheckoutTimer = setInterval(() => this.forceCheckoutIfIdle(), IDLE_CHECKOUT_POLL_MS);
|
|
767
820
|
if (typeof document !== "undefined") {
|
|
768
821
|
this.visibilityListener = () => this.forceCheckoutIfIdle();
|
|
@@ -770,9 +823,11 @@ class RingBufferRecorder {
|
|
|
770
823
|
}
|
|
771
824
|
}
|
|
772
825
|
stop() {
|
|
773
|
-
var _a;
|
|
826
|
+
var _a, _b;
|
|
774
827
|
(_a = this.stopFn) == null ? void 0 : _a.call(this);
|
|
775
828
|
this.stopFn = null;
|
|
829
|
+
(_b = this.stopCanvasTracking) == null ? void 0 : _b.call(this);
|
|
830
|
+
this.stopCanvasTracking = null;
|
|
776
831
|
if (this.idleCheckoutTimer !== null) {
|
|
777
832
|
clearInterval(this.idleCheckoutTimer);
|
|
778
833
|
this.idleCheckoutTimer = null;
|
|
@@ -839,7 +894,6 @@ class RingBufferRecorder {
|
|
|
839
894
|
return serializeSnapshot({
|
|
840
895
|
segments,
|
|
841
896
|
redactEvents: redactReplayEvents,
|
|
842
|
-
windowSeconds: spanSeconds(segments),
|
|
843
897
|
rrwebVersion: RRWEB_VERSION,
|
|
844
898
|
privacyConfigVersion: cfg.version
|
|
845
899
|
});
|
|
@@ -872,17 +926,31 @@ function withoutBody(log) {
|
|
|
872
926
|
}
|
|
873
927
|
return stripped;
|
|
874
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
|
+
}
|
|
875
941
|
function trimLogBackupPayload(payload, budgetBytes = LOG_BACKUP_BUDGET_BYTES) {
|
|
942
|
+
var _a;
|
|
876
943
|
if (payloadSizeBytes(payload) <= budgetBytes) return payload;
|
|
877
944
|
let trimmed = {
|
|
878
945
|
network: payload.network.map(withoutBody),
|
|
879
|
-
console: payload.console
|
|
946
|
+
console: payload.console,
|
|
947
|
+
socket: (_a = payload.socket) == null ? void 0 : _a.map(withoutFrameData)
|
|
880
948
|
};
|
|
881
949
|
while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.network.length > 0) {
|
|
882
|
-
trimmed = { network: trimmed.network.slice(1)
|
|
950
|
+
trimmed = { ...trimmed, network: trimmed.network.slice(1) };
|
|
883
951
|
}
|
|
884
952
|
while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.console.length > 0) {
|
|
885
|
-
trimmed = {
|
|
953
|
+
trimmed = { ...trimmed, console: trimmed.console.slice(1) };
|
|
886
954
|
}
|
|
887
955
|
return trimmed;
|
|
888
956
|
}
|
|
@@ -1031,13 +1099,29 @@ const DB_NAME = "issue-sticker-replay";
|
|
|
1031
1099
|
const DB_VERSION = 1;
|
|
1032
1100
|
const STORE_NAME = "segments";
|
|
1033
1101
|
const REPLAY_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
+
);
|
|
1037
1121
|
}
|
|
1038
|
-
function trimReplayBackup(segments,
|
|
1122
|
+
function trimReplayBackup(segments, sizeBudget = REPLAY_BACKUP_SIZE_BUDGET) {
|
|
1039
1123
|
let trimmed = segments;
|
|
1040
|
-
while (trimmed.length > 1 &&
|
|
1124
|
+
while (trimmed.length > 1 && sizeOfSegments(trimmed) > sizeBudget) {
|
|
1041
1125
|
trimmed = trimmed.slice(1);
|
|
1042
1126
|
}
|
|
1043
1127
|
return trimmed;
|
|
@@ -1238,6 +1322,8 @@ function createReplaySession(options = {}) {
|
|
|
1238
1322
|
backup.schedule();
|
|
1239
1323
|
};
|
|
1240
1324
|
const recorder = options.recorder ?? new RingBufferRecorder({
|
|
1325
|
+
...options.trackCanvasInteractions === false ? { trackCanvasInteractions: false } : {},
|
|
1326
|
+
...options.windowMs === void 0 ? {} : { windowMs: options.windowMs },
|
|
1241
1327
|
privacyConfig: options.privacyConfig,
|
|
1242
1328
|
onChange: scheduleBackup
|
|
1243
1329
|
});
|
|
@@ -1320,7 +1406,11 @@ class SessionReplayCaptureController {
|
|
|
1320
1406
|
this.setSnapshot({ available: false, canAttach: false, error: null });
|
|
1321
1407
|
return;
|
|
1322
1408
|
}
|
|
1323
|
-
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
|
+
});
|
|
1324
1414
|
recorder.start();
|
|
1325
1415
|
this.recorder = recorder;
|
|
1326
1416
|
this.setSnapshot({ available: true, canAttach, error: null });
|
|
@@ -1356,8 +1446,8 @@ class SessionReplayCaptureController {
|
|
|
1356
1446
|
this.stopRecorder();
|
|
1357
1447
|
this.listeners.clear();
|
|
1358
1448
|
}
|
|
1359
|
-
createRecorder(
|
|
1360
|
-
return this.options.createRecorder ? this.options.createRecorder(
|
|
1449
|
+
createRecorder(config) {
|
|
1450
|
+
return this.options.createRecorder ? this.options.createRecorder(config) : createReplaySession(config);
|
|
1361
1451
|
}
|
|
1362
1452
|
stopRecorder() {
|
|
1363
1453
|
var _a;
|
|
@@ -1372,179 +1462,6 @@ class SessionReplayCaptureController {
|
|
|
1372
1462
|
this.listeners.forEach((listener) => listener(this.snapshot));
|
|
1373
1463
|
}
|
|
1374
1464
|
}
|
|
1375
|
-
function forEachElementDeep(root, callback) {
|
|
1376
|
-
if (root instanceof Element) {
|
|
1377
|
-
callback(root);
|
|
1378
|
-
if (root.shadowRoot) {
|
|
1379
|
-
forEachElementDeep(root.shadowRoot, callback);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
Array.from(root.children).forEach((child) => {
|
|
1383
|
-
forEachElementDeep(child, callback);
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
const SCREENSHOT_FULL_MASK_SELECTOR = SESSION_REPLAY_FULL_MASK_SELECTOR;
|
|
1387
|
-
const SCREENSHOT_BLOCK_SELECTOR = SESSION_REPLAY_BLOCK_SELECTOR;
|
|
1388
|
-
const SCREENSHOT_MASK_BACKGROUND = "rgba(148, 148, 148, 0.28)";
|
|
1389
|
-
const SCREENSHOT_BLOCK_BACKGROUND = "rgba(148, 148, 148, 0.85)";
|
|
1390
|
-
const MASK_LABEL_WIDTH = 44;
|
|
1391
|
-
const MASK_LABEL_HEIGHT = 16;
|
|
1392
|
-
const MASK_LABEL_IMAGE = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(
|
|
1393
|
-
`<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>`
|
|
1394
|
-
)}")`;
|
|
1395
|
-
const MASK_LABEL_MIN_WIDTH = MASK_LABEL_WIDTH + 8;
|
|
1396
|
-
const MASK_LABEL_MIN_HEIGHT = MASK_LABEL_HEIGHT + 8;
|
|
1397
|
-
const TEXT_HIDING_PROPERTIES = [
|
|
1398
|
-
["color", "transparent"],
|
|
1399
|
-
["-webkit-text-fill-color", "transparent"],
|
|
1400
|
-
["text-shadow", "none"],
|
|
1401
|
-
["fill", "transparent"]
|
|
1402
|
-
];
|
|
1403
|
-
const REPLACED_MEDIA_SELECTOR = "img, svg, video, canvas, picture, iframe, object, embed";
|
|
1404
|
-
const VALUE_ELEMENT_SELECTOR = "input, textarea, select";
|
|
1405
|
-
const SENSITIVE_AUTOCOMPLETE_TOKENS = /* @__PURE__ */ new Set([
|
|
1406
|
-
// 자격증명
|
|
1407
|
-
"current-password",
|
|
1408
|
-
"new-password",
|
|
1409
|
-
"one-time-code",
|
|
1410
|
-
// 결제
|
|
1411
|
-
"cc-number",
|
|
1412
|
-
"cc-exp",
|
|
1413
|
-
"cc-exp-month",
|
|
1414
|
-
"cc-exp-year",
|
|
1415
|
-
"cc-csc",
|
|
1416
|
-
"cc-name",
|
|
1417
|
-
"cc-type",
|
|
1418
|
-
"cc-given-name",
|
|
1419
|
-
"cc-additional-name",
|
|
1420
|
-
"cc-family-name",
|
|
1421
|
-
// 신원
|
|
1422
|
-
"name",
|
|
1423
|
-
"given-name",
|
|
1424
|
-
"additional-name",
|
|
1425
|
-
"family-name",
|
|
1426
|
-
"honorific-prefix",
|
|
1427
|
-
"honorific-suffix",
|
|
1428
|
-
"nickname",
|
|
1429
|
-
"bday",
|
|
1430
|
-
"bday-day",
|
|
1431
|
-
"bday-month",
|
|
1432
|
-
"bday-year",
|
|
1433
|
-
"sex",
|
|
1434
|
-
// 주소
|
|
1435
|
-
"street-address",
|
|
1436
|
-
"address-line1",
|
|
1437
|
-
"address-line2",
|
|
1438
|
-
"address-line3",
|
|
1439
|
-
"postal-code",
|
|
1440
|
-
// 연락처
|
|
1441
|
-
"tel",
|
|
1442
|
-
"tel-national",
|
|
1443
|
-
"tel-local",
|
|
1444
|
-
"tel-area-code",
|
|
1445
|
-
"email",
|
|
1446
|
-
"impp"
|
|
1447
|
-
]);
|
|
1448
|
-
function redactScreenshotText(text) {
|
|
1449
|
-
return redactEmails(redactSensitiveValues(text));
|
|
1450
|
-
}
|
|
1451
|
-
function matchesSafely(element, selector) {
|
|
1452
|
-
try {
|
|
1453
|
-
return element.matches(selector);
|
|
1454
|
-
} catch {
|
|
1455
|
-
return false;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
function closestSafely(element, selector) {
|
|
1459
|
-
if (!element) return null;
|
|
1460
|
-
try {
|
|
1461
|
-
return element.closest(selector);
|
|
1462
|
-
} catch {
|
|
1463
|
-
return null;
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
function hasSensitiveValueHint(element) {
|
|
1467
|
-
var _a;
|
|
1468
|
-
if (((_a = element.getAttribute("type")) == null ? void 0 : _a.toLowerCase()) === "password") {
|
|
1469
|
-
return true;
|
|
1470
|
-
}
|
|
1471
|
-
const autocomplete = element.getAttribute("autocomplete");
|
|
1472
|
-
if (!autocomplete) return false;
|
|
1473
|
-
return autocomplete.toLowerCase().split(/\s+/).some((token) => SENSITIVE_AUTOCOMPLETE_TOKENS.has(token));
|
|
1474
|
-
}
|
|
1475
|
-
const overwriteProperty = (element, name, value) => {
|
|
1476
|
-
const style = element.style;
|
|
1477
|
-
style == null ? void 0 : style.setProperty(name, value, "important");
|
|
1478
|
-
};
|
|
1479
|
-
function coverMaskedRegion(region, setProperty) {
|
|
1480
|
-
setProperty(region, "background-color", SCREENSHOT_MASK_BACKGROUND);
|
|
1481
|
-
forEachElementDeep(region, (node) => {
|
|
1482
|
-
if (!isElementFullyMasked(node)) return;
|
|
1483
|
-
TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(node, name, value));
|
|
1484
|
-
if (matchesSafely(node, REPLACED_MEDIA_SELECTOR)) {
|
|
1485
|
-
setProperty(node, "visibility", "hidden");
|
|
1486
|
-
}
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
function parsePixels(value) {
|
|
1490
|
-
const parsed = Number.parseFloat(value ?? "");
|
|
1491
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
1492
|
-
}
|
|
1493
|
-
function fitsMaskLabel(region) {
|
|
1494
|
-
const rect = region.getBoundingClientRect();
|
|
1495
|
-
const style = region.style;
|
|
1496
|
-
const width = rect.width || parsePixels(style == null ? void 0 : style.width);
|
|
1497
|
-
const height = rect.height || parsePixels(style == null ? void 0 : style.height);
|
|
1498
|
-
return width >= MASK_LABEL_MIN_WIDTH && height >= MASK_LABEL_MIN_HEIGHT;
|
|
1499
|
-
}
|
|
1500
|
-
function coverBlockedRegion(region, setProperty) {
|
|
1501
|
-
const labeled = fitsMaskLabel(region);
|
|
1502
|
-
setProperty(region, "visibility", "visible");
|
|
1503
|
-
setProperty(region, "background-color", SCREENSHOT_BLOCK_BACKGROUND);
|
|
1504
|
-
setProperty(region, "background-image", labeled ? MASK_LABEL_IMAGE : "none");
|
|
1505
|
-
if (labeled) {
|
|
1506
|
-
setProperty(region, "background-repeat", "no-repeat");
|
|
1507
|
-
setProperty(region, "background-position", "center");
|
|
1508
|
-
setProperty(region, "background-size", "auto");
|
|
1509
|
-
}
|
|
1510
|
-
TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(region, name, value));
|
|
1511
|
-
setProperty(region, "pointer-events", "none");
|
|
1512
|
-
forEachElementDeep(region, (descendant) => {
|
|
1513
|
-
if (descendant !== region) {
|
|
1514
|
-
setProperty(descendant, "visibility", "hidden");
|
|
1515
|
-
}
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
function coverPrivateRegions(root, setProperty) {
|
|
1519
|
-
const blocked = [];
|
|
1520
|
-
const masked = [];
|
|
1521
|
-
forEachElementDeep(root, (element) => {
|
|
1522
|
-
if (matchesSafely(element, SCREENSHOT_BLOCK_SELECTOR)) {
|
|
1523
|
-
blocked.push(element);
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
if (matchesSafely(element, SCREENSHOT_FULL_MASK_SELECTOR) && isElementFullyMasked(element)) {
|
|
1527
|
-
masked.push(element);
|
|
1528
|
-
}
|
|
1529
|
-
});
|
|
1530
|
-
masked.forEach((region) => coverMaskedRegion(region, setProperty));
|
|
1531
|
-
blocked.forEach((region) => coverBlockedRegion(region, setProperty));
|
|
1532
|
-
}
|
|
1533
|
-
function applyScreenshotPrivacyMask(clonedRoot) {
|
|
1534
|
-
coverPrivateRegions(clonedRoot, overwriteProperty);
|
|
1535
|
-
const walker = clonedRoot.ownerDocument.createTreeWalker(clonedRoot, NodeFilter.SHOW_TEXT);
|
|
1536
|
-
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
1537
|
-
const text = node.nodeValue;
|
|
1538
|
-
if (!text) continue;
|
|
1539
|
-
node.nodeValue = redactScreenshotText(text);
|
|
1540
|
-
}
|
|
1541
|
-
clonedRoot.querySelectorAll(VALUE_ELEMENT_SELECTOR).forEach((element) => {
|
|
1542
|
-
const value = element.getAttribute("value");
|
|
1543
|
-
if (!value) return;
|
|
1544
|
-
const hidden = closestSafely(element, SCREENSHOT_BLOCK_SELECTOR) !== null || isElementFullyMasked(element) || hasSensitiveValueHint(element);
|
|
1545
|
-
element.setAttribute("value", hidden ? "" : redactScreenshotText(value));
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
1465
|
const SCREENSHOT_DATA_URL_QUALITY = 0.95;
|
|
1549
1466
|
let nextCaptureRunId = 0;
|
|
1550
1467
|
function createCaptureAttributeNames() {
|
|
@@ -1763,10 +1680,18 @@ function blobToDataUrl(blob) {
|
|
|
1763
1680
|
reader.readAsDataURL(blob);
|
|
1764
1681
|
});
|
|
1765
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
|
+
}
|
|
1766
1690
|
async function captureWithModernScreenshot(options) {
|
|
1767
1691
|
const { domToBlob } = await loadModernScreenshot();
|
|
1768
1692
|
const { element, excludeSelectors = [], width, height, scale = getCaptureScale() } = options;
|
|
1769
1693
|
await waitForDocumentFonts();
|
|
1694
|
+
const canvasFrames = await grabCanvasFrames(element, excludeSelectors);
|
|
1770
1695
|
const attributeNames = createCaptureAttributeNames();
|
|
1771
1696
|
const stickySnapshot = createModernStickySnapshot(element, attributeNames);
|
|
1772
1697
|
const excludeSnapshot = createExcludeElementSnapshot(element, excludeSelectors, attributeNames);
|
|
@@ -1793,8 +1718,21 @@ async function captureWithModernScreenshot(options) {
|
|
|
1793
1718
|
if (height) {
|
|
1794
1719
|
captureOptions.height = height;
|
|
1795
1720
|
}
|
|
1796
|
-
|
|
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
|
+
);
|
|
1797
1734
|
} finally {
|
|
1735
|
+
canvasFrames.forEach((frame) => frame.bitmap.close());
|
|
1798
1736
|
excludeSnapshot.cleanup();
|
|
1799
1737
|
stickySnapshot.cleanup();
|
|
1800
1738
|
}
|
|
@@ -1879,7 +1817,7 @@ async function calculateSha256Hex(file) {
|
|
|
1879
1817
|
}
|
|
1880
1818
|
function normalizeUploadMimeType(mimeType, mediaType) {
|
|
1881
1819
|
var _a;
|
|
1882
|
-
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";
|
|
1883
1821
|
const normalized = (_a = (mimeType || fallback).split(";", 1)[0]) == null ? void 0 : _a.trim().toLowerCase();
|
|
1884
1822
|
return normalized && normalized.length > 0 ? normalized : fallback;
|
|
1885
1823
|
}
|
|
@@ -4190,8 +4128,8 @@ function pristineFetch(input, init) {
|
|
|
4190
4128
|
}
|
|
4191
4129
|
return typeof window !== "undefined" ? window.fetch(input, init) : fetch(input, init);
|
|
4192
4130
|
}
|
|
4193
|
-
const INSTRUMENTATION_FAILURE_LIMIT = 5;
|
|
4194
|
-
const REPATCH_LIMIT = 2;
|
|
4131
|
+
const INSTRUMENTATION_FAILURE_LIMIT$1 = 5;
|
|
4132
|
+
const REPATCH_LIMIT$1 = 2;
|
|
4195
4133
|
function setInternalField(target, key, value) {
|
|
4196
4134
|
Object.defineProperty(target, key, {
|
|
4197
4135
|
value,
|
|
@@ -4288,7 +4226,7 @@ function omitTooLargeResponseBody(sizeBytes) {
|
|
|
4288
4226
|
responseBodySizeBytes: sizeBytes
|
|
4289
4227
|
};
|
|
4290
4228
|
}
|
|
4291
|
-
function bytesToBase64(bytes) {
|
|
4229
|
+
function bytesToBase64$1(bytes) {
|
|
4292
4230
|
let binary = "";
|
|
4293
4231
|
const chunkSize = 32768;
|
|
4294
4232
|
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
@@ -4298,7 +4236,7 @@ function bytesToBase64(bytes) {
|
|
|
4298
4236
|
return btoa(binary);
|
|
4299
4237
|
}
|
|
4300
4238
|
async function arrayBufferToBase64(arrayBuffer) {
|
|
4301
|
-
return bytesToBase64(new Uint8Array(arrayBuffer));
|
|
4239
|
+
return bytesToBase64$1(new Uint8Array(arrayBuffer));
|
|
4302
4240
|
}
|
|
4303
4241
|
function getResponseDetailLevel(response) {
|
|
4304
4242
|
if (response.type === "opaque" || response.type === "opaqueredirect") return "opaque";
|
|
@@ -4415,7 +4353,7 @@ class NetworkCollector {
|
|
|
4415
4353
|
ensurePatched() {
|
|
4416
4354
|
if (!this.running) return;
|
|
4417
4355
|
if (window.fetch === this.patchedFetch) return;
|
|
4418
|
-
if (this.repatchCount >= REPATCH_LIMIT) return;
|
|
4356
|
+
if (this.repatchCount >= REPATCH_LIMIT$1) return;
|
|
4419
4357
|
this.repatchCount += 1;
|
|
4420
4358
|
this.interceptFetch();
|
|
4421
4359
|
}
|
|
@@ -4461,7 +4399,7 @@ class NetworkCollector {
|
|
|
4461
4399
|
*/
|
|
4462
4400
|
recordInstrumentationFailure() {
|
|
4463
4401
|
this.instrumentationFailures += 1;
|
|
4464
|
-
if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT) {
|
|
4402
|
+
if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT$1) {
|
|
4465
4403
|
this.stop();
|
|
4466
4404
|
}
|
|
4467
4405
|
}
|
|
@@ -4683,6 +4621,422 @@ class NetworkCollector {
|
|
|
4683
4621
|
this.patchedXHR = null;
|
|
4684
4622
|
}
|
|
4685
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
|
+
}
|
|
4686
5040
|
var NetworkLogType = /* @__PURE__ */ ((NetworkLogType2) => {
|
|
4687
5041
|
NetworkLogType2["MAIN_FRAME"] = "main_frame";
|
|
4688
5042
|
NetworkLogType2["SUB_FRAME"] = "sub_frame";
|
|
@@ -14230,6 +14584,20 @@ function filterExcludeServiceRelatedConsoleLogs(consoleLogs) {
|
|
|
14230
14584
|
return !isIssueStickerInternalConsoleLog(log) && !isIssueStickerServiceConsoleLog(log);
|
|
14231
14585
|
});
|
|
14232
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
|
+
}
|
|
14233
14601
|
function buildExternalUserPayload(user) {
|
|
14234
14602
|
if (!user) return {};
|
|
14235
14603
|
return {
|
|
@@ -14238,60 +14606,63 @@ function buildExternalUserPayload(user) {
|
|
|
14238
14606
|
};
|
|
14239
14607
|
}
|
|
14240
14608
|
export {
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
|
|
14609
|
+
Check as $,
|
|
14610
|
+
trimLogBackupPayload as A,
|
|
14611
|
+
createSafeLogBackup as B,
|
|
14244
14612
|
ConsoleCollector as C,
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
14249
|
-
|
|
14613
|
+
getLogBackupSessionId as D,
|
|
14614
|
+
createIndexedDbLogBackup as E,
|
|
14615
|
+
PopoverTrigger as F,
|
|
14616
|
+
Input as G,
|
|
14617
|
+
PopoverContent as H,
|
|
14250
14618
|
ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
|
|
14251
|
-
|
|
14252
|
-
|
|
14619
|
+
Select$1 as J,
|
|
14620
|
+
SelectTrigger as K,
|
|
14253
14621
|
Label as L,
|
|
14254
|
-
|
|
14622
|
+
SelectValue as M,
|
|
14255
14623
|
NetworkCollector as N,
|
|
14256
|
-
|
|
14624
|
+
SelectContent as O,
|
|
14257
14625
|
Popover as P,
|
|
14258
|
-
|
|
14259
|
-
|
|
14626
|
+
SelectItem as Q,
|
|
14627
|
+
getJiraMetadata as R,
|
|
14260
14628
|
ScreenRecordingController as S,
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14265
|
-
|
|
14266
|
-
|
|
14267
|
-
|
|
14268
|
-
|
|
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 _,
|
|
14269
14637
|
uploadSignedMediaWithApi as a,
|
|
14270
|
-
|
|
14271
|
-
|
|
14272
|
-
|
|
14273
|
-
|
|
14274
|
-
|
|
14275
|
-
|
|
14276
|
-
|
|
14277
|
-
|
|
14278
|
-
|
|
14279
|
-
|
|
14280
|
-
|
|
14281
|
-
|
|
14282
|
-
|
|
14283
|
-
|
|
14284
|
-
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
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,
|
|
14295
14666
|
buildJiraCreateFields as b,
|
|
14296
14667
|
captureWithModernScreenshot as c,
|
|
14297
14668
|
directUploadToSignedUrl as d,
|
|
@@ -14312,9 +14683,9 @@ export {
|
|
|
14312
14683
|
selectNetworkLogsForSubmit as s,
|
|
14313
14684
|
sanitizeString as t,
|
|
14314
14685
|
uploadScreenshotDataUrls as u,
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
|
|
14319
|
-
|
|
14686
|
+
serializeSocketTranscript as v,
|
|
14687
|
+
uploadSocketTranscript as w,
|
|
14688
|
+
getCurrentPageUrl as x,
|
|
14689
|
+
SessionReplayCaptureController as y,
|
|
14690
|
+
preloadModernScreenshot as z
|
|
14320
14691
|
};
|