@sailfish-ai/recorder 1.11.6 → 1.12.4
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/README.md +19 -1
- package/dist/chunkSerializer.js +70 -23
- package/dist/chunkSerializer.js.br +0 -0
- package/dist/chunkSerializer.js.gz +0 -0
- package/dist/chunks/chunkSerializer-DDukZpgl.js +116 -0
- package/dist/chunks/chunkSerializer-DDukZpgl.js.br +0 -0
- package/dist/chunks/chunkSerializer-DDukZpgl.js.gz +0 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js +115 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js.br +0 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js.gz +0 -0
- package/dist/chunks/{index-CftVmmO9.js → index-C-qbsfKe.js} +703 -542
- package/dist/chunks/index-C-qbsfKe.js.br +0 -0
- package/dist/chunks/index-C-qbsfKe.js.gz +0 -0
- package/dist/chunks/{index-QXHuV98g.js → index-D6axlCRu.js} +751 -586
- package/dist/chunks/index-D6axlCRu.js.br +0 -0
- package/dist/chunks/index-D6axlCRu.js.gz +0 -0
- package/dist/clockSync.js +196 -0
- package/dist/clockSync.js.br +0 -0
- package/dist/clockSync.js.gz +0 -0
- package/dist/graphql.js +5 -0
- package/dist/graphql.js.br +0 -0
- package/dist/graphql.js.gz +0 -0
- package/dist/inAppReportIssueModal/index.js +4 -1
- package/dist/inAppReportIssueModal/index.js.br +0 -0
- package/dist/inAppReportIssueModal/index.js.gz +0 -0
- package/dist/inAppReportIssueModal/integrations.js +36 -0
- package/dist/inAppReportIssueModal/integrations.js.br +0 -0
- package/dist/inAppReportIssueModal/integrations.js.gz +0 -0
- package/dist/inAppReportIssueModal/state.js +8 -0
- package/dist/inAppReportIssueModal/state.js.br +0 -0
- package/dist/inAppReportIssueModal/state.js.gz +0 -0
- package/dist/index.js +67 -5
- package/dist/index.js.br +0 -0
- package/dist/index.js.gz +0 -0
- package/dist/privacyMask.js +93 -0
- package/dist/privacyMask.js.br +0 -0
- package/dist/privacyMask.js.gz +0 -0
- package/dist/recorder.cjs +2 -2
- package/dist/recorder.cjs.br +0 -0
- package/dist/recorder.cjs.gz +0 -0
- package/dist/recorder.js +25 -22
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +1310 -1127
- package/dist/recorder.umd.cjs.br +0 -0
- package/dist/recorder.umd.cjs.gz +0 -0
- package/dist/recording.js +84 -13
- package/dist/recording.js.br +0 -0
- package/dist/recording.js.gz +0 -0
- package/dist/types/chunkSerializer.d.ts +14 -0
- package/dist/types/clockSync.d.ts +70 -0
- package/dist/types/inAppReportIssueModal/integrations.d.ts +1 -0
- package/dist/types/inAppReportIssueModal/state.d.ts +2 -0
- package/dist/types/index.d.ts +16 -2
- package/dist/types/privacyMask.d.ts +46 -0
- package/dist/types/recording.d.ts +1 -0
- package/dist/types/types.d.ts +23 -0
- package/dist/types/websocket.d.ts +1 -0
- package/dist/websocket.js +111 -0
- package/dist/websocket.js.br +0 -0
- package/dist/websocket.js.gz +0 -0
- package/package.json +1 -1
- package/dist/chunks/chunkSerializer-D7_uK-dD.js +0 -94
- package/dist/chunks/chunkSerializer-D7_uK-dD.js.br +0 -0
- package/dist/chunks/chunkSerializer-D7_uK-dD.js.gz +0 -0
- package/dist/chunks/chunkSerializer-DkWXHnW4.js +0 -95
- package/dist/chunks/chunkSerializer-DkWXHnW4.js.br +0 -0
- package/dist/chunks/chunkSerializer-DkWXHnW4.js.gz +0 -0
- package/dist/chunks/index-CftVmmO9.js.br +0 -0
- package/dist/chunks/index-CftVmmO9.js.gz +0 -0
- package/dist/chunks/index-QXHuV98g.js.br +0 -0
- package/dist/chunks/index-QXHuV98g.js.gz +0 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Browser → server clock synchronization.
|
|
2
|
+
//
|
|
3
|
+
// Browser wall time (Date.now()) is untrustworthy: users can have wrong system
|
|
4
|
+
// clocks, NTP can jump them, sleep/resume can drift them, and they can be
|
|
5
|
+
// spoofed. We need every event to carry a server-comparable timestamp.
|
|
6
|
+
//
|
|
7
|
+
// Strategy: NTP-style handshake over the existing WebSocket. The browser
|
|
8
|
+
// records performance.now() before and after each handshake; the server
|
|
9
|
+
// returns its receive/send wall times. From those we compute RTT and a
|
|
10
|
+
// midpoint-aligned estimate of (serverWall - browserMonotonic), which we then
|
|
11
|
+
// add to performance.now() at any later moment to estimate server wall time.
|
|
12
|
+
//
|
|
13
|
+
// performance.now() is preferred over Date.now() because it is monotonic and
|
|
14
|
+
// unaffected by user clock changes mid-session.
|
|
15
|
+
const HAS_WINDOW = typeof window !== "undefined";
|
|
16
|
+
const HAS_PERFORMANCE = typeof performance !== "undefined" && typeof performance.now === "function";
|
|
17
|
+
// Caps how many samples we retain. We only ever rank-select a few; the rest
|
|
18
|
+
// are pruned on age so we don't grow unbounded across a long session.
|
|
19
|
+
const MAX_SAMPLES = 20;
|
|
20
|
+
const MAX_SAMPLE_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
|
21
|
+
// Filter cutoff: samples with RTT above this are not considered usable.
|
|
22
|
+
const MAX_USABLE_RTT_MS = 500;
|
|
23
|
+
// Number of best (lowest-RTT) samples we take the median over.
|
|
24
|
+
const BEST_SAMPLE_COUNT = 5;
|
|
25
|
+
// Browser monotonic clock. Falls back to Date.now() when performance is
|
|
26
|
+
// unavailable (SSR / very old runtimes).
|
|
27
|
+
export function monotonicNow() {
|
|
28
|
+
return HAS_PERFORMANCE ? performance.now() : Date.now();
|
|
29
|
+
}
|
|
30
|
+
export function timeOriginMs() {
|
|
31
|
+
return HAS_PERFORMANCE ? performance.timeOrigin : 0;
|
|
32
|
+
}
|
|
33
|
+
class ClockSyncManager {
|
|
34
|
+
samples = [];
|
|
35
|
+
pending = new Map();
|
|
36
|
+
estimatedOffsetMs = null;
|
|
37
|
+
bestRttMs = null;
|
|
38
|
+
lastSyncMonoMs = null;
|
|
39
|
+
// Begins a sync exchange. The caller is responsible for actually sending
|
|
40
|
+
// the request payload — we just record what we need to correlate the reply.
|
|
41
|
+
beginSync(requestId) {
|
|
42
|
+
const entry = {
|
|
43
|
+
requestId,
|
|
44
|
+
clientSendMonoMs: monotonicNow(),
|
|
45
|
+
};
|
|
46
|
+
this.pending.set(requestId, entry);
|
|
47
|
+
return entry;
|
|
48
|
+
}
|
|
49
|
+
// Drops a pending request without recording a sample. Use when a request
|
|
50
|
+
// times out or the websocket closes while it was in flight.
|
|
51
|
+
abandonSync(requestId) {
|
|
52
|
+
this.pending.delete(requestId);
|
|
53
|
+
}
|
|
54
|
+
// Process a server reply. Returns the sample we recorded, or null if the
|
|
55
|
+
// requestId was unknown (late reply after abandon, duplicate, etc.).
|
|
56
|
+
recordTimeSyncResponse(body) {
|
|
57
|
+
const pending = this.pending.get(body.requestId);
|
|
58
|
+
if (!pending)
|
|
59
|
+
return null;
|
|
60
|
+
this.pending.delete(body.requestId);
|
|
61
|
+
const clientReceiveMonoMs = monotonicNow();
|
|
62
|
+
const rttMs = clientReceiveMonoMs - pending.clientSendMonoMs;
|
|
63
|
+
if (rttMs < 0 || !isFinite(rttMs))
|
|
64
|
+
return null;
|
|
65
|
+
const clientMidpointMono = (pending.clientSendMonoMs + clientReceiveMonoMs) / 2;
|
|
66
|
+
const serverMidpointWall = (body.serverReceivedAtMs + body.serverSentAtMs) / 2;
|
|
67
|
+
const estimatedOffsetMs = serverMidpointWall - clientMidpointMono;
|
|
68
|
+
const sample = {
|
|
69
|
+
clientSendMonoMs: pending.clientSendMonoMs,
|
|
70
|
+
clientReceiveMonoMs,
|
|
71
|
+
serverReceiveWallMs: body.serverReceivedAtMs,
|
|
72
|
+
serverSendWallMs: body.serverSentAtMs,
|
|
73
|
+
rttMs,
|
|
74
|
+
estimatedOffsetMs,
|
|
75
|
+
browserWallAtSyncMs: Date.now(),
|
|
76
|
+
};
|
|
77
|
+
this.samples.push(sample);
|
|
78
|
+
this.pruneSamples(clientReceiveMonoMs);
|
|
79
|
+
this.recompute(clientReceiveMonoMs);
|
|
80
|
+
return sample;
|
|
81
|
+
}
|
|
82
|
+
// Returns null when no estimate is available yet.
|
|
83
|
+
estimateServerTime(monoMs) {
|
|
84
|
+
if (this.estimatedOffsetMs == null)
|
|
85
|
+
return null;
|
|
86
|
+
return this.estimatedOffsetMs + monoMs;
|
|
87
|
+
}
|
|
88
|
+
getSyncMetadata() {
|
|
89
|
+
const now = monotonicNow();
|
|
90
|
+
const syncAgeMs = this.lastSyncMonoMs != null ? now - this.lastSyncMonoMs : null;
|
|
91
|
+
return {
|
|
92
|
+
offsetMs: this.estimatedOffsetMs,
|
|
93
|
+
rttMs: this.bestRttMs,
|
|
94
|
+
syncAgeMs,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Difference between browser wall clock and estimated server wall clock at
|
|
98
|
+
// the time of the most recent sample. Positive means browser clock is ahead.
|
|
99
|
+
getBrowserClockSkewMs() {
|
|
100
|
+
if (this.samples.length === 0)
|
|
101
|
+
return null;
|
|
102
|
+
const latest = this.samples[this.samples.length - 1];
|
|
103
|
+
const serverWallAtLatest = latest.estimatedOffsetMs + latest.clientReceiveMonoMs;
|
|
104
|
+
return latest.browserWallAtSyncMs - serverWallAtLatest;
|
|
105
|
+
}
|
|
106
|
+
reset() {
|
|
107
|
+
this.samples = [];
|
|
108
|
+
this.pending.clear();
|
|
109
|
+
this.estimatedOffsetMs = null;
|
|
110
|
+
this.bestRttMs = null;
|
|
111
|
+
this.lastSyncMonoMs = null;
|
|
112
|
+
}
|
|
113
|
+
pruneSamples(nowMono) {
|
|
114
|
+
if (this.samples.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
const cutoff = nowMono - MAX_SAMPLE_AGE_MS;
|
|
117
|
+
this.samples = this.samples.filter((s) => s.clientReceiveMonoMs >= cutoff);
|
|
118
|
+
if (this.samples.length > MAX_SAMPLES) {
|
|
119
|
+
this.samples = this.samples.slice(this.samples.length - MAX_SAMPLES);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
recompute(nowMono) {
|
|
123
|
+
const usable = this.samples
|
|
124
|
+
.filter((s) => s.rttMs <= MAX_USABLE_RTT_MS)
|
|
125
|
+
.sort((a, b) => a.rttMs - b.rttMs)
|
|
126
|
+
.slice(0, BEST_SAMPLE_COUNT);
|
|
127
|
+
if (usable.length === 0) {
|
|
128
|
+
// No usable samples; keep last known offset but mark no-recent-sync.
|
|
129
|
+
// (We still update lastSyncMonoMs so the caller can see we tried.)
|
|
130
|
+
this.lastSyncMonoMs = nowMono;
|
|
131
|
+
this.bestRttMs = null;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const offsets = usable
|
|
135
|
+
.map((s) => s.estimatedOffsetMs)
|
|
136
|
+
.sort((a, b) => a - b);
|
|
137
|
+
this.estimatedOffsetMs = offsets[Math.floor(offsets.length / 2)];
|
|
138
|
+
this.bestRttMs = usable[0].rttMs;
|
|
139
|
+
this.lastSyncMonoMs = nowMono;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Singleton — every consumer in the SDK shares one offset state. Created
|
|
143
|
+
// lazily so SSR imports don't need any browser globals.
|
|
144
|
+
let _instance = null;
|
|
145
|
+
export function getClockSyncManager() {
|
|
146
|
+
if (!_instance) {
|
|
147
|
+
_instance = new ClockSyncManager();
|
|
148
|
+
}
|
|
149
|
+
return _instance;
|
|
150
|
+
}
|
|
151
|
+
// Convenience wrappers for callers that don't care about the instance.
|
|
152
|
+
export function estimateServerTime(monoMs) {
|
|
153
|
+
return getClockSyncManager().estimateServerTime(monoMs);
|
|
154
|
+
}
|
|
155
|
+
export function getClockSyncMetadata() {
|
|
156
|
+
return getClockSyncManager().getSyncMetadata();
|
|
157
|
+
}
|
|
158
|
+
// Builds the `client` and `serverEstimated` envelope attached to every event.
|
|
159
|
+
// `capturedMonoMs` should be performance.now() captured at the moment the
|
|
160
|
+
// event occurred (not when it was eventually flushed).
|
|
161
|
+
export function buildEventTimeEnvelope(capturedMonoMs, capturedWallMs) {
|
|
162
|
+
const mgr = getClockSyncManager();
|
|
163
|
+
const meta = mgr.getSyncMetadata();
|
|
164
|
+
const eventTimeMs = mgr.estimateServerTime(capturedMonoMs);
|
|
165
|
+
return {
|
|
166
|
+
client: {
|
|
167
|
+
wallTimeMs: capturedWallMs,
|
|
168
|
+
monotonicMs: capturedMonoMs,
|
|
169
|
+
timeOriginMs: timeOriginMs(),
|
|
170
|
+
},
|
|
171
|
+
serverEstimated: {
|
|
172
|
+
eventTimeMs,
|
|
173
|
+
offsetMs: meta.offsetMs,
|
|
174
|
+
rttMs: meta.rttMs,
|
|
175
|
+
syncAgeMs: meta.syncAgeMs,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Capture-at-source helper for callers that want to stamp the moment the
|
|
180
|
+
// event happened, not the moment sendEvent runs (e.g. after a network body
|
|
181
|
+
// has finished reading). The enrichment step will reuse these values.
|
|
182
|
+
export function captureClientTime() {
|
|
183
|
+
return {
|
|
184
|
+
wallTimeMs: Date.now(),
|
|
185
|
+
monotonicMs: monotonicNow(),
|
|
186
|
+
timeOriginMs: timeOriginMs(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Exported for tests only.
|
|
190
|
+
export const __testing = {
|
|
191
|
+
reset: () => getClockSyncManager().reset(),
|
|
192
|
+
MAX_USABLE_RTT_MS,
|
|
193
|
+
BEST_SAMPLE_COUNT,
|
|
194
|
+
HAS_WINDOW,
|
|
195
|
+
HAS_PERFORMANCE,
|
|
196
|
+
};
|
|
Binary file
|
|
Binary file
|
package/dist/graphql.js
CHANGED
package/dist/graphql.js.br
CHANGED
|
Binary file
|
package/dist/graphql.js.gz
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createTriageAndIssueFromRecorder, createTriageFromRecorder, } from "../graphql";
|
|
2
2
|
import { getDefaultReporterAccountId, getFieldsForProject, getIntegrationData, getProjectsForTeam, getSprintFieldId, getUsers, getValidSavedReporterAccountId, hasValidIntegration, refreshIntegrationData, saveLastReporterAccountId, updateFormWithIntegrationData, updateIssueTypeOptions, } from "./integrations";
|
|
3
|
-
import { currentState, isRecording, recordingEndTime, recordingStartTime, resetState, setIsRecording, setRecordingEndTime, setRecordingStartTime, setTimerInterval, timerInterval, } from "./state";
|
|
3
|
+
import { currentState, isRecording, recordingEndTime, recordingStartTime, resetState, setIsRecording, setRecordingEndTime, setRecordingStartTime, setSprintDefaulted, setTimerInterval, timerInterval, } from "./state";
|
|
4
4
|
import { STORAGE_KEYS } from "./types";
|
|
5
5
|
import { getChevronSVG, renderCustomMultiSelect, renderDynamicField, } from "./ui";
|
|
6
6
|
// TODO - enable configuration by keyboard typing in UI
|
|
@@ -980,6 +980,9 @@ function bindEngTicketListeners() {
|
|
|
980
980
|
engSprintSelect.addEventListener("change", () => {
|
|
981
981
|
currentState.engTicketSprint = engSprintSelect.value;
|
|
982
982
|
engSprintSelect.style.color = engSprintSelect.value ? "" : "#9ca3af";
|
|
983
|
+
// User made an explicit choice (including clearing) — don't let a
|
|
984
|
+
// later integration data refetch override it with the auto-default.
|
|
985
|
+
setSprintDefaulted(true);
|
|
983
986
|
});
|
|
984
987
|
}
|
|
985
988
|
if (engPrioritySelect) {
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchEngineeringTicketPlatformIntegrations } from "../graphql";
|
|
2
2
|
import { getIdentifiedUser } from "../sendSailfishMessages";
|
|
3
|
+
import { sprintDefaulted, setSprintDefaulted } from "./state";
|
|
3
4
|
const SUPPORT_TICKET_INTEGRATIONS = ["jira", "linear", "zendesk"];
|
|
4
5
|
let integrationData = null;
|
|
5
6
|
// Cache per cloud so switching between clouds is instant
|
|
@@ -124,6 +125,27 @@ export function populatePriorityOptions(selectElement, provider, defaultPriority
|
|
|
124
125
|
selectElement.value = "0";
|
|
125
126
|
}
|
|
126
127
|
}
|
|
128
|
+
// Mirrors useEngineeringTicketForm.ts:_projectBoardIds — when the chosen
|
|
129
|
+
// project exposes its boards, prefer an active sprint whose originBoardId
|
|
130
|
+
// matches one of them so multi-board Jira projects don't grab a sprint
|
|
131
|
+
// from the wrong board.
|
|
132
|
+
function _projectBoardIds(projectId, projects) {
|
|
133
|
+
if (!projectId)
|
|
134
|
+
return [];
|
|
135
|
+
const project = (projects || []).find((p) => p.id === projectId || p.key === projectId);
|
|
136
|
+
const boards = project?.boards || project?.board_ids || [];
|
|
137
|
+
return boards.map((b) => (typeof b === "object" ? b.id : b));
|
|
138
|
+
}
|
|
139
|
+
export function pickDefaultActiveSprint(sprints, projectId, projects) {
|
|
140
|
+
const activeSprints = (sprints || []).filter((s) => s.state === "active");
|
|
141
|
+
if (!activeSprints.length)
|
|
142
|
+
return null;
|
|
143
|
+
const projectBoardIds = _projectBoardIds(projectId, projects);
|
|
144
|
+
const projectScoped = projectBoardIds.length
|
|
145
|
+
? activeSprints.find((s) => projectBoardIds.some((bid) => String(bid) === String(s.originBoardId)))
|
|
146
|
+
: null;
|
|
147
|
+
return projectScoped || activeSprints[0];
|
|
148
|
+
}
|
|
127
149
|
export function populateSprintOptions(selectElement, sprints, currentValue) {
|
|
128
150
|
selectElement.innerHTML = "";
|
|
129
151
|
// Placeholder option
|
|
@@ -407,7 +429,21 @@ export function updateFormWithIntegrationData(currentState) {
|
|
|
407
429
|
const sprintSelect = document.getElementById("sf-eng-ticket-sprint");
|
|
408
430
|
const isJira = integrationData.provider?.toLowerCase() === "jira";
|
|
409
431
|
if (sprintSelect && isJira && integrationData.sprints) {
|
|
432
|
+
// Default to the current active sprint on first population so Report
|
|
433
|
+
// Issue-created Jira tickets land in the current sprint, matching the
|
|
434
|
+
// dashboard EngineeringTicketModal's auto-default behavior
|
|
435
|
+
// (useEngineeringTicketForm.ts:956-991).
|
|
436
|
+
if (!currentState.engTicketSprint && !sprintDefaulted) {
|
|
437
|
+
const chosen = pickDefaultActiveSprint(integrationData.sprints, currentState.engTicketProject, integrationData.projects || []);
|
|
438
|
+
if (chosen?.id) {
|
|
439
|
+
currentState.engTicketSprint = String(chosen.id);
|
|
440
|
+
setSprintDefaulted(true);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
410
443
|
populateSprintOptions(sprintSelect, integrationData.sprints, currentState.engTicketSprint || undefined);
|
|
444
|
+
if (currentState.engTicketSprint) {
|
|
445
|
+
sprintSelect.style.color = "";
|
|
446
|
+
}
|
|
411
447
|
}
|
|
412
448
|
// Update issue type dropdown based on selected project (for Jira)
|
|
413
449
|
const issueTypeSelect = document.getElementById("sf-eng-ticket-type");
|
|
Binary file
|
|
Binary file
|
|
@@ -35,6 +35,10 @@ export let recordingStartTime = null;
|
|
|
35
35
|
export let recordingEndTime = null;
|
|
36
36
|
export let timerInterval = null;
|
|
37
37
|
export let isRecording = false;
|
|
38
|
+
// Tracks whether the Jira sprint dropdown has already been auto-defaulted
|
|
39
|
+
// for this modal session, so refetches of integration data don't override
|
|
40
|
+
// a user-cleared/changed value.
|
|
41
|
+
export let sprintDefaulted = false;
|
|
38
42
|
// State setters
|
|
39
43
|
export function setCurrentState(state) {
|
|
40
44
|
currentState = state;
|
|
@@ -43,6 +47,10 @@ export function resetState() {
|
|
|
43
47
|
currentState = getInitialState();
|
|
44
48
|
recordingStartTime = null;
|
|
45
49
|
recordingEndTime = null;
|
|
50
|
+
sprintDefaulted = false;
|
|
51
|
+
}
|
|
52
|
+
export function setSprintDefaulted(value) {
|
|
53
|
+
sprintDefaulted = value;
|
|
46
54
|
}
|
|
47
55
|
export function setRecordingStartTime(time) {
|
|
48
56
|
recordingStartTime = time;
|
|
Binary file
|
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -11,10 +11,11 @@ import { sendMapUuidIfAvailable } from "./mapUuid";
|
|
|
11
11
|
import { getUrlAndStoredUuids, initializeConsolePlugin, initializeDomContentEvents, initializePerformancePlugin, initializeRecording, invalidateUrlCache, } from "./recording";
|
|
12
12
|
import { HAS_DOCUMENT, HAS_LOCAL_STORAGE, HAS_SESSION_STORAGE, HAS_WINDOW, } from "./runtimeEnv";
|
|
13
13
|
import { isHeadlessOrLighthouse } from "./headlessDetection";
|
|
14
|
+
import { captureClientTime } from "./clockSync";
|
|
14
15
|
import { ensureSessionListeners, getOrSetSessionId } from "./session";
|
|
15
16
|
import { withAppUrlMetadata } from "./utils";
|
|
16
17
|
import { onNavigationChange } from "./websocket";
|
|
17
|
-
import { clearStaleFuncSpanState, getFuncSpanHeader, isFunctionSpanTrackingEnabled, restoreFuncSpanState, sendEvent, sendMessage, } from "./websocket";
|
|
18
|
+
import { clearStaleFuncSpanState, getFuncSpanHeader, isFunctionSpanTrackingEnabled, requestTimeSync, restoreFuncSpanState, sendEvent, sendMessage, } from "./websocket";
|
|
18
19
|
import { yieldToMain } from "./scheduler";
|
|
19
20
|
import { detectFramework } from "./frameworkDetection";
|
|
20
21
|
const DEBUG = readDebugFlag(); // A wrapper around fetch that suppresses connection refused errors
|
|
@@ -73,6 +74,13 @@ export const DEFAULT_CAPTURE_SETTINGS = {
|
|
|
73
74
|
recordDob: false,
|
|
74
75
|
sampling: {},
|
|
75
76
|
enableFiberTracking: false,
|
|
77
|
+
// Form Privacy Rules — empty = no rule. Mirrors the backend defaults so the
|
|
78
|
+
// recorder behaves identically whether the server payload arrives or not.
|
|
79
|
+
maskInputSelector: "",
|
|
80
|
+
maskTextSelector: "",
|
|
81
|
+
blockSelector: "",
|
|
82
|
+
unmaskSelector: "",
|
|
83
|
+
maskInputOptions: { password: true },
|
|
76
84
|
};
|
|
77
85
|
export const DEFAULT_CONSOLE_RECORDING_SETTINGS = {
|
|
78
86
|
level: ["info", "log", "warn", "error"],
|
|
@@ -154,6 +162,22 @@ function trackDomainChangesOnce() {
|
|
|
154
162
|
sessionStorage.setItem("prevPageVisitUUID", prevPageVisitUUID);
|
|
155
163
|
invalidateUrlCache();
|
|
156
164
|
const timestamp = Date.now();
|
|
165
|
+
// Opt-in diagnostic log for the duplicate-/-skipped page_visit_uuid bug.
|
|
166
|
+
// Gated by enableInternalDebugLogs (passed via initRecorder); off by
|
|
167
|
+
// default so customer integrations see no console noise. Captures the
|
|
168
|
+
// call stack so we can tell which trigger fired (pushState, replaceState,
|
|
169
|
+
// popstate, the 1s poll, or the initial forced mint).
|
|
170
|
+
if (g.internalDebugLogs) {
|
|
171
|
+
// eslint-disable-next-line no-console
|
|
172
|
+
console.log("[sf-recorder] mint pageVisitUUID", {
|
|
173
|
+
url: currentDomain,
|
|
174
|
+
newUuid: pageVisitUUID,
|
|
175
|
+
prevUuid: prevPageVisitUUID,
|
|
176
|
+
forceSend,
|
|
177
|
+
timestamp,
|
|
178
|
+
stack: new Error().stack,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
157
181
|
sendMessage({
|
|
158
182
|
type: "routeChange",
|
|
159
183
|
data: {
|
|
@@ -226,6 +250,9 @@ function handleVisibilityChange() {
|
|
|
226
250
|
// Restore sessionId when the user returns to the page
|
|
227
251
|
if (visibilityState === "visible") {
|
|
228
252
|
getOrSetSessionId();
|
|
253
|
+
// Background tabs commonly suffer clock drift (sleep/resume); refresh the
|
|
254
|
+
// browser→server offset before any post-resume events fire.
|
|
255
|
+
requestTimeSync();
|
|
229
256
|
}
|
|
230
257
|
// Send visibility change event for both visible and hidden states
|
|
231
258
|
try {
|
|
@@ -285,6 +312,11 @@ function _ensureModuleSideEffects() {
|
|
|
285
312
|
window.addEventListener("beforeunload", () => {
|
|
286
313
|
clearPageVisitDataFromSessionStorage();
|
|
287
314
|
});
|
|
315
|
+
// Reconnection is a strong trigger for stale clock offset (DHCP-driven
|
|
316
|
+
// NTP correction often happens here).
|
|
317
|
+
window.addEventListener("online", () => {
|
|
318
|
+
requestTimeSync();
|
|
319
|
+
});
|
|
288
320
|
}
|
|
289
321
|
}
|
|
290
322
|
function storeCredentialsAndConnection({ apiKey, backendApi, }) {
|
|
@@ -354,6 +386,23 @@ function matchParsedUrlAgainstPatterns(parsed, patterns) {
|
|
|
354
386
|
return true;
|
|
355
387
|
});
|
|
356
388
|
}
|
|
389
|
+
// Resolve a captured request URL to absolute form before it is transmitted.
|
|
390
|
+
// Idempotent: already-absolute URLs (https:, blob:, chrome-extension:) pass
|
|
391
|
+
// through unchanged; relative/protocol-relative URLs gain the page origin.
|
|
392
|
+
// SSR-safe: never touches `window` unguarded (see veritas/jsts-frontend/CLAUDE.md).
|
|
393
|
+
export function toAbsoluteUrl(url) {
|
|
394
|
+
if (typeof url !== "string" || url.length === 0)
|
|
395
|
+
return url;
|
|
396
|
+
try {
|
|
397
|
+
const base = typeof window !== "undefined"
|
|
398
|
+
? window.location.href
|
|
399
|
+
: "http://localhost/";
|
|
400
|
+
return new URL(url, base).href;
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return url;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
357
406
|
// Public wrapper: parse + delegate. Preserved for callers outside the gate
|
|
358
407
|
// (interceptor wrappers, tests). Hot paths should call
|
|
359
408
|
// matchParsedUrlAgainstPatterns() directly with a pre-parsed URL.
|
|
@@ -515,7 +564,7 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = [], body
|
|
|
515
564
|
success,
|
|
516
565
|
error: errorMsg,
|
|
517
566
|
method: this._requestMethod,
|
|
518
|
-
url,
|
|
567
|
+
url: toAbsoluteUrl(url),
|
|
519
568
|
request_headers: requestHeaders,
|
|
520
569
|
request_body: requestBody,
|
|
521
570
|
response_headers: responseHeaders,
|
|
@@ -790,6 +839,9 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = [], bodyCaptureC
|
|
|
790
839
|
isRetry = true;
|
|
791
840
|
}
|
|
792
841
|
const endTime = Date.now();
|
|
842
|
+
// Stamp client.* at the moment the response arrived; body capture may
|
|
843
|
+
// delay sendEvent() by hundreds of ms for streaming/large bodies.
|
|
844
|
+
const requestCompletionTime = captureClientTime();
|
|
793
845
|
const status = response.status;
|
|
794
846
|
const success = response.ok;
|
|
795
847
|
const error = success ? "" : `Request Error: ${response.statusText}`;
|
|
@@ -812,6 +864,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = [], bodyCaptureC
|
|
|
812
864
|
type: NetworkRequestEventId,
|
|
813
865
|
timestamp: endTime,
|
|
814
866
|
sessionId,
|
|
867
|
+
client: requestCompletionTime,
|
|
815
868
|
data: {
|
|
816
869
|
request_id: networkUUID,
|
|
817
870
|
session_id: sessionId,
|
|
@@ -821,7 +874,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = [], bodyCaptureC
|
|
|
821
874
|
success,
|
|
822
875
|
error,
|
|
823
876
|
method,
|
|
824
|
-
url,
|
|
877
|
+
url: toAbsoluteUrl(url),
|
|
825
878
|
retry_without_trace_id: isRetry,
|
|
826
879
|
request_headers: requestHeaders,
|
|
827
880
|
request_body: requestBody,
|
|
@@ -930,7 +983,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = [], bodyCaptureC
|
|
|
930
983
|
success,
|
|
931
984
|
error: errorMessage,
|
|
932
985
|
method,
|
|
933
|
-
url,
|
|
986
|
+
url: toAbsoluteUrl(url),
|
|
934
987
|
request_headers: requestHeaders,
|
|
935
988
|
request_body: resolvedRequestBody,
|
|
936
989
|
response_body: null,
|
|
@@ -1040,7 +1093,7 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
|
|
|
1040
1093
|
// Pass [] to fully disable header propagation. Pass exact patterns
|
|
1041
1094
|
// (e.g. ["api.myapp.com", "*.internal.com"]) to restrict propagation
|
|
1042
1095
|
// to a known set of domains.
|
|
1043
|
-
domainsToPropagateHeaderTo = ["*"], domainsToNotPropagateHeaderTo = [], serviceVersion, serviceIdentifier, gitSha, serviceAdditionalMetadata, enableIpTracking, captureStreamingResponseBody = true, captureResponseBodyMaxMb = 10, captureStreamPrefixKb = 64, captureStreamTimeoutMs = 10000, enableFiberTracking = false, deferRecording, deferRecordingStart, chunkSnapshot, useWsWorker = true, capturePerformanceMetrics = true, maskTextClass, library, headlessRecording = false, }) {
|
|
1096
|
+
domainsToPropagateHeaderTo = ["*"], domainsToNotPropagateHeaderTo = [], serviceVersion, serviceIdentifier, gitSha, serviceAdditionalMetadata, enableIpTracking, captureStreamingResponseBody = true, captureResponseBodyMaxMb = 10, captureStreamPrefixKb = 64, captureStreamTimeoutMs = 10000, enableFiberTracking = false, deferRecording, deferRecordingStart, chunkSnapshot, useWsWorker = true, capturePerformanceMetrics = true, maskTextClass, maskInputSelector, maskTextSelector, blockSelector, unmaskSelector, maskInputOptions, library, headlessRecording = false, }) {
|
|
1044
1097
|
// Synthetic-environment no-op: Lighthouse/PSI, HeadlessChrome, WebPageTest
|
|
1045
1098
|
// (PTST), Puppeteer/Playwright/Selenium (navigator.webdriver). We skip init
|
|
1046
1099
|
// entirely to avoid WSS retry noise, third-party perf penalties in audits,
|
|
@@ -1196,6 +1249,12 @@ domainsToPropagateHeaderTo = ["*"], domainsToNotPropagateHeaderTo = [], serviceV
|
|
|
1196
1249
|
// Caller-supplied maskTextClass wins over server-fetched value; only
|
|
1197
1250
|
// applied when explicitly set so undefined cannot clobber the server.
|
|
1198
1251
|
...(maskTextClass !== undefined ? { maskTextClass } : {}),
|
|
1252
|
+
// Form Privacy Rules — same caller-overrides-when-defined pattern.
|
|
1253
|
+
...(maskInputSelector !== undefined ? { maskInputSelector } : {}),
|
|
1254
|
+
...(maskTextSelector !== undefined ? { maskTextSelector } : {}),
|
|
1255
|
+
...(blockSelector !== undefined ? { blockSelector } : {}),
|
|
1256
|
+
...(unmaskSelector !== undefined ? { unmaskSelector } : {}),
|
|
1257
|
+
...(maskInputOptions !== undefined ? { maskInputOptions } : {}),
|
|
1199
1258
|
};
|
|
1200
1259
|
// If a socket is already open now, stop here.
|
|
1201
1260
|
if (g.ws && g.ws.readyState === 1) {
|
|
@@ -1233,6 +1292,9 @@ export const initRecorder = async (options) => {
|
|
|
1233
1292
|
if (typeof window === "undefined")
|
|
1234
1293
|
return;
|
|
1235
1294
|
const g = (window.__sailfish_recorder ||= {});
|
|
1295
|
+
// Stash the debug flag on the global so trackDomainChangesOnce and other
|
|
1296
|
+
// helpers can read it without being passed through every call site.
|
|
1297
|
+
g.internalDebugLogs = options.enableInternalDebugLogs === true;
|
|
1236
1298
|
const currentSessionId = getOrSetSessionId();
|
|
1237
1299
|
// remove stale page visit data from previous sessions
|
|
1238
1300
|
clearPageVisitDataFromSessionStorage();
|
package/dist/index.js.br
CHANGED
|
Binary file
|
package/dist/index.js.gz
CHANGED
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Helpers that apply account-level Form Privacy Rules to rrweb's masking
|
|
2
|
+
// hooks. Kept in their own module so they can be unit-tested without
|
|
3
|
+
// pulling in the rest of recording.tsx (which imports rrweb dynamically).
|
|
4
|
+
/**
|
|
5
|
+
* Safe ancestor-match check. A malformed selector — typo'd by an admin in
|
|
6
|
+
* the Form Privacy Rules UI — would otherwise throw from `closest()` and
|
|
7
|
+
* crash rrweb's serializer mid-walk. Under-masking is better than dropping
|
|
8
|
+
* the session.
|
|
9
|
+
*/
|
|
10
|
+
export function closestSafe(el, selector) {
|
|
11
|
+
if (!el || !selector)
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
return !!el.closest(selector);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Element.matches with try/catch for malformed selectors. */
|
|
21
|
+
export function matchesSelectorSafe(el, selector) {
|
|
22
|
+
if (!selector)
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
return el.matches(selector);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the maskInputFn we hand to rrweb. rrweb only calls maskInputFn after
|
|
33
|
+
* its own check on `maskInputOptions[tagName] || maskInputOptions[type]` —
|
|
34
|
+
* it has NO `maskInputSelector` concept. To honor admin-set
|
|
35
|
+
* `maskInputSelector` in normal record() mode, callers expand the
|
|
36
|
+
* rrweb-facing `maskInputOptions` to all-true so rrweb routes EVERY input
|
|
37
|
+
* through this fn. This function then enforces the real per-element decision:
|
|
38
|
+
*
|
|
39
|
+
* 1. unmaskSelector match → raw (admin opted this subtree out)
|
|
40
|
+
* 2. type in admin-intent maskInputOptions OR maskInputSelector match → mask
|
|
41
|
+
* 3. otherwise → raw (admin didn't configure this field for masking)
|
|
42
|
+
*
|
|
43
|
+
* `baseFn` produces the masked string (PII-specific last-4 CC/SSN/DOB, or
|
|
44
|
+
* a full-asterisk fallback).
|
|
45
|
+
*/
|
|
46
|
+
export function buildMaskInputFn(opts) {
|
|
47
|
+
const { baseFn, unmaskSelector, maskInputSelector, adminMaskInputOptions } = opts;
|
|
48
|
+
// Without any per-element rule there's nothing to decide; rrweb's own
|
|
49
|
+
// maskInputOptions check already gates the call and baseFn does the rest.
|
|
50
|
+
if (!unmaskSelector && !maskInputSelector)
|
|
51
|
+
return baseFn;
|
|
52
|
+
return (text, node) => {
|
|
53
|
+
if (node.type === "hidden")
|
|
54
|
+
return "";
|
|
55
|
+
if (closestSafe(node, unmaskSelector))
|
|
56
|
+
return text;
|
|
57
|
+
const type = (node.type || "").toLowerCase();
|
|
58
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
59
|
+
const typeMatches = adminMaskInputOptions[type] === true ||
|
|
60
|
+
adminMaskInputOptions[tag] === true;
|
|
61
|
+
const selectorMatches = matchesSelectorSafe(node, maskInputSelector);
|
|
62
|
+
if (!typeMatches && !selectorMatches) {
|
|
63
|
+
// Admin didn't configure this field; pass raw through.
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
return baseFn(text, node);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Use buildMaskInputFn. Kept so older test imports keep working
|
|
71
|
+
* during the transition.
|
|
72
|
+
*/
|
|
73
|
+
export function wrapMaskInputFn(baseFn, unmaskSelector) {
|
|
74
|
+
if (!unmaskSelector)
|
|
75
|
+
return baseFn;
|
|
76
|
+
return (text, node) => {
|
|
77
|
+
if (closestSafe(node, unmaskSelector))
|
|
78
|
+
return text;
|
|
79
|
+
return baseFn(text, node);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Default maskTextFn — replaces every non-whitespace char with "*",
|
|
84
|
+
* matching rrweb's built-in behavior. Wrapped so that nodes under
|
|
85
|
+
* `unmaskSelector` skip masking.
|
|
86
|
+
*/
|
|
87
|
+
export function makeMaskTextFn(unmaskSelector) {
|
|
88
|
+
return (text, element) => {
|
|
89
|
+
if (closestSafe(element, unmaskSelector))
|
|
90
|
+
return text;
|
|
91
|
+
return text.replace(/\S/g, "*");
|
|
92
|
+
};
|
|
93
|
+
}
|
|
Binary file
|
|
Binary file
|
package/dist/recorder.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const e = require("./chunks/index-
|
|
4
|
-
exports.DEFAULT_CAPTURE_SETTINGS = e.DEFAULT_CAPTURE_SETTINGS, exports.DEFAULT_CONSOLE_RECORDING_SETTINGS = e.DEFAULT_CONSOLE_RECORDING_SETTINGS, exports.STORAGE_VERSION = e.STORAGE_VERSION, exports.addOrUpdateMetadata = e.addOrUpdateMetadata, exports.buildBatches = e.buildBatches, exports.clearStaleFuncSpanState = e.clearStaleFuncSpanState, exports.createSkipHeadersPropagationChecker = e.createSkipHeadersPropagationChecker, exports.createTriageAndIssueFromRecorder = e.createTriageAndIssueFromRecorder, exports.createTriageFromRecorder = e.createTriageFromRecorder, exports.disableFunctionSpanTracking = e.disableFunctionSpanTracking, exports.enableFunctionSpanTracking = e.enableFunctionSpanTracking, exports.ensureHrefCache = e.ensureHrefCache, exports.eventSize = e.eventSize, exports.fetchAndSendIp = e.fetchAndSendIp, exports.fetchCaptureSettings = e.fetchCaptureSettings, exports.fetchEngineeringTicketPlatformIntegrations = e.fetchEngineeringTicketPlatformIntegrations, exports.fetchFunctionSpanTrackingEnabled = e.fetchFunctionSpanTrackingEnabled, exports.flushBufferedEvents = e.flushBufferedEvents, exports.getCachedHref = e.getCachedHref, exports.getCachedHrefNoQuery = e.getCachedHrefNoQuery, exports.getFuncSpanHeader = e.getFuncSpanHeader, exports.getIdentifiedUser = e.getIdentifiedUser, exports.getOrSetSessionId = e.getOrSetSessionId, exports.getUrlAndStoredUuids = e.getUrlAndStoredUuids, exports.identify = e.identify, exports.initRecorder = e.initRecorder, exports.initializeConsolePlugin = e.initializeConsolePlugin, exports.initializeDomContentEvents = e.initializeDomContentEvents, exports.initializeFunctionSpanTrackingFromApi = e.initializeFunctionSpanTrackingFromApi, exports.initializePerformancePlugin = e.initializePerformancePlugin, exports.initializeRecording = e.initializeRecording, exports.initializeWebSocket = e.initializeWebSocket, exports.invalidateUrlCache = e.invalidateUrlCache, exports.isFunctionSpanTrackingEnabled = e.isFunctionSpanTrackingEnabled, exports.matchUrlWithWildcard = e.matchUrlWithWildcard, Object.defineProperty(exports, "nowTimestamp", { enumerable: true, get: () => e.nowTimestamp }), exports.onNavigationChange = e.onNavigationChange, exports.openReportIssueModal = e.openReportIssueModal, exports.restoreFuncSpanState = e.restoreFuncSpanState, exports.sendDomainsToNotPropagateHeaderTo = e.sendDomainsToNotPropagateHeaderTo, exports.sendEvent = e.sendEvent, exports.sendGraphQLRequest = e.sendGraphQLRequest, exports.sendMessage = e.sendMessage, exports.startRecording = e.startRecording, exports.startRecordingSession = e.startRecordingSession, exports.trackingEvent = e.trackingEvent, exports.withAppUrlMetadata = e.withAppUrlMetadata;
|
|
3
|
+
const e = require("./chunks/index-C-qbsfKe.js");
|
|
4
|
+
exports.DEFAULT_CAPTURE_SETTINGS = e.DEFAULT_CAPTURE_SETTINGS, exports.DEFAULT_CONSOLE_RECORDING_SETTINGS = e.DEFAULT_CONSOLE_RECORDING_SETTINGS, exports.STORAGE_VERSION = e.STORAGE_VERSION, exports.addOrUpdateMetadata = e.addOrUpdateMetadata, exports.buildBatches = e.buildBatches, exports.clearStaleFuncSpanState = e.clearStaleFuncSpanState, exports.createSkipHeadersPropagationChecker = e.createSkipHeadersPropagationChecker, exports.createTriageAndIssueFromRecorder = e.createTriageAndIssueFromRecorder, exports.createTriageFromRecorder = e.createTriageFromRecorder, exports.disableFunctionSpanTracking = e.disableFunctionSpanTracking, exports.enableFunctionSpanTracking = e.enableFunctionSpanTracking, exports.ensureHrefCache = e.ensureHrefCache, exports.eventSize = e.eventSize, exports.fetchAndSendIp = e.fetchAndSendIp, exports.fetchCaptureSettings = e.fetchCaptureSettings, exports.fetchEngineeringTicketPlatformIntegrations = e.fetchEngineeringTicketPlatformIntegrations, exports.fetchFunctionSpanTrackingEnabled = e.fetchFunctionSpanTrackingEnabled, exports.flushBufferedEvents = e.flushBufferedEvents, exports.getCachedHref = e.getCachedHref, exports.getCachedHrefNoQuery = e.getCachedHrefNoQuery, exports.getFuncSpanHeader = e.getFuncSpanHeader, exports.getIdentifiedUser = e.getIdentifiedUser, exports.getOrSetSessionId = e.getOrSetSessionId, exports.getUrlAndStoredUuids = e.getUrlAndStoredUuids, exports.identify = e.identify, exports.initRecorder = e.initRecorder, exports.initializeConsolePlugin = e.initializeConsolePlugin, exports.initializeDomContentEvents = e.initializeDomContentEvents, exports.initializeFunctionSpanTrackingFromApi = e.initializeFunctionSpanTrackingFromApi, exports.initializePerformancePlugin = e.initializePerformancePlugin, exports.initializeRecording = e.initializeRecording, exports.initializeWebSocket = e.initializeWebSocket, exports.invalidateUrlCache = e.invalidateUrlCache, exports.isFunctionSpanTrackingEnabled = e.isFunctionSpanTrackingEnabled, exports.maskInputFn = e.maskInputFn, exports.matchUrlWithWildcard = e.matchUrlWithWildcard, Object.defineProperty(exports, "nowTimestamp", { enumerable: true, get: () => e.nowTimestamp }), exports.onNavigationChange = e.onNavigationChange, exports.openReportIssueModal = e.openReportIssueModal, exports.requestTimeSync = e.requestTimeSync, exports.restoreFuncSpanState = e.restoreFuncSpanState, exports.sendDomainsToNotPropagateHeaderTo = e.sendDomainsToNotPropagateHeaderTo, exports.sendEvent = e.sendEvent, exports.sendGraphQLRequest = e.sendGraphQLRequest, exports.sendMessage = e.sendMessage, exports.startRecording = e.startRecording, exports.startRecordingSession = e.startRecordingSession, exports.toAbsoluteUrl = e.toAbsoluteUrl, exports.trackingEvent = e.trackingEvent, exports.withAppUrlMetadata = e.withAppUrlMetadata;
|
package/dist/recorder.cjs.br
CHANGED
|
Binary file
|
package/dist/recorder.cjs.gz
CHANGED
|
Binary file
|