@sailfish-ai/recorder 1.7.49 → 1.8.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/dist/env.js +55 -0
- package/dist/errorInterceptor.js +14 -5
- package/dist/exponentialBackoff.js +2 -2
- package/dist/graphql.js +2 -1
- package/dist/index.js +106 -65
- package/dist/recording.js +91 -29
- package/dist/sailfish-recorder.cjs.js +1 -1
- package/dist/sailfish-recorder.cjs.js.br +0 -0
- package/dist/sailfish-recorder.cjs.js.gz +0 -0
- package/dist/sailfish-recorder.es.js +1 -1
- package/dist/sailfish-recorder.es.js.br +0 -0
- package/dist/sailfish-recorder.es.js.gz +0 -0
- package/dist/sailfish-recorder.umd.js +1 -1
- package/dist/sailfish-recorder.umd.js.br +0 -0
- package/dist/sailfish-recorder.umd.js.gz +0 -0
- package/dist/types/env.d.ts +2 -0
- package/dist/types/utils.d.ts +1 -0
- package/dist/utils.js +6 -0
- package/dist/websocket.js +17 -7
- package/package.json +4 -9
package/dist/env.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Avoid referencing `import.meta` in distributed code.
|
|
2
|
+
// Priority: explicit global → process.env → default false.
|
|
3
|
+
export function readDebugFlag() {
|
|
4
|
+
// 1) Global (works in any bundler and vanilla <script> usage)
|
|
5
|
+
try {
|
|
6
|
+
const g = globalThis;
|
|
7
|
+
if (typeof g.__SAILFISH_DEBUG__ === "boolean")
|
|
8
|
+
return g.__SAILFISH_DEBUG__;
|
|
9
|
+
}
|
|
10
|
+
catch { }
|
|
11
|
+
// 2) process.env (webpack/rollup/esbuild all replace at build-time)
|
|
12
|
+
try {
|
|
13
|
+
const p = typeof process !== "undefined" ? process : undefined;
|
|
14
|
+
if (p?.env?.SAILFISH_DEBUG != null) {
|
|
15
|
+
const v = String(p.env.SAILFISH_DEBUG).toLowerCase();
|
|
16
|
+
return v === "1" || v === "true" || v === "yes";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
// 3) Build-time define fallback (we'll inject __SAILFISH_DEBUG__ in vite.config)
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const D = globalThis.__SAILFISH_DEBUG_DEFINE__;
|
|
24
|
+
if (typeof D === "boolean")
|
|
25
|
+
return D;
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// Build-time injected Git SHA (without using `import.meta`).
|
|
31
|
+
export function readGitSha() {
|
|
32
|
+
// 1) global
|
|
33
|
+
try {
|
|
34
|
+
const g = globalThis;
|
|
35
|
+
if (typeof g.__GIT_SHA__ === "string" && g.__GIT_SHA__)
|
|
36
|
+
return g.__GIT_SHA__;
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
// 2) process.env
|
|
40
|
+
try {
|
|
41
|
+
const p = typeof process !== "undefined" ? process : undefined;
|
|
42
|
+
if (p?.env?.GIT_SHA)
|
|
43
|
+
return String(p.env.GIT_SHA);
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
// 3) build-time define fallback (see vite.config define)
|
|
47
|
+
try {
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
const S = globalThis.__GIT_SHA_DEFINE__;
|
|
50
|
+
if (typeof S === "string" && S)
|
|
51
|
+
return S;
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
package/dist/errorInterceptor.js
CHANGED
|
@@ -8,7 +8,9 @@ import { sendMessage } from "./websocket";
|
|
|
8
8
|
export async function resolveStackTrace(stackTrace) {
|
|
9
9
|
if (!stackTrace)
|
|
10
10
|
return ["No stack trace available"];
|
|
11
|
-
const traceLines = Array.isArray(stackTrace)
|
|
11
|
+
const traceLines = Array.isArray(stackTrace)
|
|
12
|
+
? stackTrace
|
|
13
|
+
: stackTrace.split("\n");
|
|
12
14
|
const mappedStack = [];
|
|
13
15
|
for (const line of traceLines) {
|
|
14
16
|
const match = line.match(/\/assets\/([^\/]+\.js):(\d+):(\d+)/);
|
|
@@ -24,7 +26,7 @@ export async function resolveStackTrace(stackTrace) {
|
|
|
24
26
|
if (!response.ok) {
|
|
25
27
|
break;
|
|
26
28
|
}
|
|
27
|
-
const rawSourceMap = await response.json();
|
|
29
|
+
const rawSourceMap = (await response.json());
|
|
28
30
|
if (!rawSourceMap.sources || !Array.isArray(rawSourceMap.sources)) {
|
|
29
31
|
mappedStack.push(line);
|
|
30
32
|
continue;
|
|
@@ -50,7 +52,9 @@ export async function resolveStackTrace(stackTrace) {
|
|
|
50
52
|
contextSnippet = lines.slice(start, end).map((line, i) => {
|
|
51
53
|
const lineNumber = start + i + 1;
|
|
52
54
|
const prefix = lineNumber === errorLine ? "👉" : " ";
|
|
53
|
-
return `${prefix} ${lineNumber
|
|
55
|
+
return `${prefix} ${lineNumber
|
|
56
|
+
.toString()
|
|
57
|
+
.padStart(4)} | ${line.trim()}`;
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
mappedStack.push(`${original.source}:${original.line}:${original.column} (${original.name || "anonymous"})`);
|
|
@@ -96,8 +100,13 @@ async function captureError(error, isPromiseRejection = false) {
|
|
|
96
100
|
};
|
|
97
101
|
// Sends the mapped error details to the backend.
|
|
98
102
|
sendMessage({
|
|
99
|
-
type: "
|
|
100
|
-
|
|
103
|
+
type: "event",
|
|
104
|
+
event: {
|
|
105
|
+
type: 6,
|
|
106
|
+
data: {
|
|
107
|
+
payload: errorDetails,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
101
110
|
});
|
|
102
111
|
}
|
|
103
112
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
// A wrapper around fetch that suppresses connection refused errors
|
|
1
|
+
import { readDebugFlag } from "./env";
|
|
2
|
+
const DEBUG = readDebugFlag(); // A wrapper around fetch that suppresses connection refused errors
|
|
3
3
|
export function silentFetch(input, init) {
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
5
|
fetch(input, init)
|
package/dist/graphql.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { exponentialBackoff } from "./exponentialBackoff"; // Import your custom backoff function
|
|
2
|
-
|
|
2
|
+
import { readDebugFlag } from "./env";
|
|
3
|
+
const DEBUG = readDebugFlag(); // A wrapper around fetch that suppresses connection refused errors
|
|
3
4
|
export function sendGraphQLRequest(operationName, query, variables, retries = 5, // Number of retries before giving up
|
|
4
5
|
initialBackoff = 2000, // Initial backoff in milliseconds
|
|
5
6
|
backoffFactor = 2) {
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import { readDebugFlag } from "./env";
|
|
2
2
|
// import { NetworkRecordOptions } from "@sailfish-rrweb/rrweb-plugin-network-record";
|
|
3
3
|
import { v4 as uuidv4 } from "uuid";
|
|
4
4
|
import { NetworkRequestEventId, STATIC_EXTENSIONS, xSf3RidHeader, } from "./constants";
|
|
5
5
|
import { gatherAndCacheDeviceInfo } from "./deviceInfo";
|
|
6
|
+
import { readGitSha } from "./env";
|
|
7
|
+
import { initializeErrorInterceptor } from "./errorInterceptor";
|
|
6
8
|
import { fetchCaptureSettings, sendDomainsToNotPropagateHeaderTo, startRecordingSession, } from "./graphql";
|
|
7
9
|
import { setupIssueReporting } from "./inAppReportIssueModal";
|
|
8
10
|
import { sendMapUuidIfAvailable } from "./mapUuid";
|
|
9
11
|
import { getUrlAndStoredUuids, initializeConsolePlugin, initializeDomContentEvents, initializeRecording, } from "./recording";
|
|
10
12
|
import { HAS_DOCUMENT, HAS_LOCAL_STORAGE, HAS_SESSION_STORAGE, HAS_WINDOW, } from "./runtimeEnv";
|
|
11
13
|
import { getOrSetSessionId } from "./session";
|
|
14
|
+
import { withAppUrlMetadata } from "./utils";
|
|
12
15
|
import { sendEvent, sendMessage } from "./websocket";
|
|
16
|
+
const DEBUG = readDebugFlag(); // A wrapper around fetch that suppresses connection refused errors
|
|
13
17
|
// Default list of domains to ignore
|
|
14
18
|
const DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT = [
|
|
15
19
|
"t.co",
|
|
@@ -78,7 +82,12 @@ export const DEFAULT_CONSOLE_RECORDING_SETTINGS = {
|
|
|
78
82
|
// recordBody: true,
|
|
79
83
|
// recordInitialRequests: false,
|
|
80
84
|
// };
|
|
81
|
-
function
|
|
85
|
+
function trackDomainChangesOnce() {
|
|
86
|
+
const g = (window.__sailfish_recorder ||= {});
|
|
87
|
+
if (g.routeWatcherIntervalId) {
|
|
88
|
+
// already running
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
82
91
|
let lastDomain = window.location.href.split("?")[0];
|
|
83
92
|
const checkDomainChange = (forceSend = false) => {
|
|
84
93
|
const currentDomain = window.location.href.split("?")[0];
|
|
@@ -103,8 +112,8 @@ function trackDomainChanges() {
|
|
|
103
112
|
const debouncedCheck = debounce(() => checkDomainChange(), 500);
|
|
104
113
|
// Force the first check to send the initial URL on load
|
|
105
114
|
checkDomainChange(true);
|
|
106
|
-
//
|
|
107
|
-
setInterval(debouncedCheck, 1000);
|
|
115
|
+
// Start a single interval and remember it
|
|
116
|
+
g.routeWatcherIntervalId = window.setInterval(debouncedCheck, 1000);
|
|
108
117
|
}
|
|
109
118
|
// Debounce utility function
|
|
110
119
|
function debounce(func, wait) {
|
|
@@ -557,37 +566,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
|
|
|
557
566
|
}
|
|
558
567
|
}
|
|
559
568
|
}
|
|
560
|
-
// Note - if you update this, you must copy this function to the documentation in docs/docs/getting-started/updating-autoinstall-prs.md.
|
|
561
|
-
function getCompileTimeGitSha() {
|
|
562
|
-
// 1) Preferred: a hard-coded global replaced at build time (all bundlers support "define")
|
|
563
|
-
// e.g. webpack DefinePlugin/esbuild/rollup/vite: __GIT_SHA__ => JSON.stringify('<sha>')
|
|
564
|
-
try {
|
|
565
|
-
const g = globalThis;
|
|
566
|
-
if (g && typeof g.__GIT_SHA__ === "string" && g.__GIT_SHA__) {
|
|
567
|
-
return g.__GIT_SHA__;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
catch { }
|
|
571
|
-
// 2) Common pattern: process.env.GIT_SHA (webpack/rollup/esbuild/parcel)
|
|
572
|
-
try {
|
|
573
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
574
|
-
const p = typeof process !== "undefined" ? process : undefined;
|
|
575
|
-
if (p?.env?.GIT_SHA)
|
|
576
|
-
return String(p.env.GIT_SHA);
|
|
577
|
-
}
|
|
578
|
-
catch { }
|
|
579
|
-
// 3) Vite (or other tools exposing import.meta.env)
|
|
580
|
-
try {
|
|
581
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
582
|
-
const im = import.meta;
|
|
583
|
-
if (im?.env?.VITE_GIT_SHA)
|
|
584
|
-
return String(im.env.VITE_GIT_SHA);
|
|
585
|
-
if (im?.env?.GIT_SHA)
|
|
586
|
-
return String(im.env.GIT_SHA);
|
|
587
|
-
}
|
|
588
|
-
catch { }
|
|
589
|
-
return undefined;
|
|
590
|
-
}
|
|
591
569
|
// --- helper for mapUuid default from window ---
|
|
592
570
|
function getMapUuidFromWindow() {
|
|
593
571
|
try {
|
|
@@ -605,37 +583,77 @@ function getMapUuidFromWindow() {
|
|
|
605
583
|
// it would be 1 serviceIdentifier per frontend user session,
|
|
606
584
|
// which is very wasteful
|
|
607
585
|
export async function startRecording({ apiKey, backendApi = "https://api-service.sailfishqa.com", domainsToPropagateHeaderTo = [], domainsToNotPropagateHeaderTo = [], serviceVersion, serviceIdentifier, gitSha, serviceAdditionalMetadata, }) {
|
|
608
|
-
const effectiveGitSha = gitSha ??
|
|
586
|
+
const effectiveGitSha = gitSha ?? readGitSha();
|
|
609
587
|
const effectiveServiceIdentifier = serviceIdentifier ?? "";
|
|
610
588
|
const effectiveServiceVersion = serviceVersion ?? "";
|
|
611
589
|
const effectiveLibrary = "JS/TS"; // from Recorder.LIBRARY_CHOICES
|
|
612
590
|
const effectiveMapUuid = getMapUuidFromWindow();
|
|
613
591
|
const sessionId = getOrSetSessionId();
|
|
614
|
-
|
|
615
|
-
|
|
592
|
+
const g = (window.__sailfish_recorder ||= {});
|
|
593
|
+
g.sessionId = sessionId;
|
|
594
|
+
g.apiKey = apiKey;
|
|
595
|
+
g.backendApi = backendApi;
|
|
596
|
+
// Already fully initialized for this session with an open socket? No-op.
|
|
597
|
+
if (g.initialized &&
|
|
598
|
+
g.sessionId === sessionId &&
|
|
599
|
+
g.ws &&
|
|
600
|
+
g.ws.readyState === 1) {
|
|
601
|
+
trackDomainChangesOnce(); // ensure single watcher alive
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
// One-time installs (per window)
|
|
605
|
+
if (!g.domEventsInit) {
|
|
606
|
+
initializeDomContentEvents(sessionId);
|
|
607
|
+
g.domEventsInit = true;
|
|
608
|
+
}
|
|
609
|
+
if (!g.consoleInit) {
|
|
610
|
+
initializeConsolePlugin(DEFAULT_CONSOLE_RECORDING_SETTINGS, sessionId);
|
|
611
|
+
g.consoleInit = true;
|
|
612
|
+
}
|
|
613
|
+
if (!g.errorInit) {
|
|
614
|
+
initializeErrorInterceptor();
|
|
615
|
+
g.errorInit = true;
|
|
616
|
+
}
|
|
616
617
|
storeCredentialsAndConnection({ apiKey, backendApi });
|
|
617
|
-
|
|
618
|
-
sessionStorage.setItem(
|
|
619
|
-
sessionStorage.setItem(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
618
|
+
trackDomainChangesOnce();
|
|
619
|
+
sessionStorage.setItem("sailfishApiKey", apiKey);
|
|
620
|
+
sessionStorage.setItem("sailfishBackendApi", backendApi);
|
|
621
|
+
if (!g.sentDoNotPropagateOnce) {
|
|
622
|
+
sendDomainsToNotPropagateHeaderTo(apiKey, [
|
|
623
|
+
...domainsToNotPropagateHeaderTo,
|
|
624
|
+
...DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT,
|
|
625
|
+
], backendApi).catch((error) => console.error("Failed to send domains to not propagate header to:", error));
|
|
626
|
+
g.sentDoNotPropagateOnce = true;
|
|
627
|
+
}
|
|
628
|
+
// Patch XHR/fetch once per window
|
|
629
|
+
if (!g.xhrPatched) {
|
|
630
|
+
setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo);
|
|
631
|
+
g.xhrPatched = true;
|
|
632
|
+
}
|
|
633
|
+
if (!g.fetchPatched) {
|
|
634
|
+
setupFetchInterceptor(domainsToNotPropagateHeaderTo);
|
|
635
|
+
g.fetchPatched = true;
|
|
636
|
+
}
|
|
627
637
|
gatherAndCacheDeviceInfo();
|
|
628
638
|
try {
|
|
629
639
|
const captureSettingsResponse = await fetchCaptureSettings(apiKey, backendApi);
|
|
630
640
|
const captureSettings = captureSettingsResponse.data?.captureSettingsFromApiKey ||
|
|
631
641
|
DEFAULT_CAPTURE_SETTINGS;
|
|
632
|
-
|
|
642
|
+
// If a socket is already open now, stop here.
|
|
643
|
+
if (g.ws && g.ws.readyState === 1) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const metadataWithAppUrl = withAppUrlMetadata(serviceAdditionalMetadata);
|
|
647
|
+
// Create/ensure a server-side recording session once per browser session
|
|
648
|
+
const sessionResponse = await startRecordingSession(apiKey, sessionId, backendApi, effectiveServiceIdentifier, effectiveServiceVersion, effectiveMapUuid, effectiveGitSha, effectiveLibrary, metadataWithAppUrl);
|
|
633
649
|
if (sessionResponse.data?.startRecordingSession) {
|
|
634
|
-
const websocket = await initializeRecording(captureSettings,
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
650
|
+
const websocket = await initializeRecording(captureSettings, backendApi, apiKey, sessionId);
|
|
651
|
+
g.ws = websocket;
|
|
652
|
+
g.initialized = true;
|
|
653
|
+
if (!g.sentMapUuidOnce) {
|
|
654
|
+
sendMapUuidIfAvailable(serviceIdentifier, serviceVersion);
|
|
655
|
+
g.sentMapUuidOnce = true;
|
|
656
|
+
}
|
|
639
657
|
}
|
|
640
658
|
else {
|
|
641
659
|
console.error("Failed to start recording session:", sessionResponse.errors || sessionResponse);
|
|
@@ -646,21 +664,44 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
|
|
|
646
664
|
}
|
|
647
665
|
}
|
|
648
666
|
export const initRecorder = async (options) => {
|
|
649
|
-
console.log("Initializing Sailfish Recorder with options:", options);
|
|
650
667
|
// Only run on the client (browser) environment
|
|
651
|
-
if (typeof window === "undefined")
|
|
668
|
+
if (typeof window === "undefined")
|
|
669
|
+
return;
|
|
670
|
+
const g = (window.__sailfish_recorder ||= {});
|
|
671
|
+
const currentSessionId = getOrSetSessionId();
|
|
672
|
+
// If already initialized for this session and socket is open, do nothing.
|
|
673
|
+
if (g.initialized &&
|
|
674
|
+
g.sessionId === currentSessionId &&
|
|
675
|
+
g.ws &&
|
|
676
|
+
g.ws.readyState === 1) {
|
|
652
677
|
return;
|
|
653
678
|
}
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
679
|
+
// Coalesce concurrent calls into one promise
|
|
680
|
+
if (!g.initPromise) {
|
|
681
|
+
g.initPromise = (async () => {
|
|
682
|
+
if (!g.hasLoggedInitOnce) {
|
|
683
|
+
// Log only once per window lifecycle to avoid noisy logs when routes remount.
|
|
684
|
+
console.log("Initializing Sailfish Recorder (first run) …");
|
|
685
|
+
g.hasLoggedInitOnce = true;
|
|
686
|
+
}
|
|
687
|
+
await startRecording(options);
|
|
688
|
+
// Set up the issue reporting UI once
|
|
689
|
+
if (!g.issueReportingInit) {
|
|
690
|
+
setupIssueReporting({
|
|
691
|
+
apiKey: options.apiKey,
|
|
692
|
+
backendApi: options.backendApi ?? "https://api-service.sailfishqa.com",
|
|
693
|
+
getSessionId: () => getOrSetSessionId(),
|
|
694
|
+
shortcuts: options.reportIssueShortcuts,
|
|
695
|
+
customBaseUrl: options.customBaseUrl,
|
|
696
|
+
});
|
|
697
|
+
g.issueReportingInit = true;
|
|
698
|
+
}
|
|
699
|
+
})().finally(() => {
|
|
700
|
+
// Keep all state/flags, but clear the temp promise so a brand-new session can re-init later
|
|
701
|
+
delete g.initPromise;
|
|
662
702
|
});
|
|
663
|
-
}
|
|
703
|
+
}
|
|
704
|
+
return g.initPromise;
|
|
664
705
|
};
|
|
665
706
|
// Re-export from other modules
|
|
666
707
|
export * from "./graphql";
|
package/dist/recording.js
CHANGED
|
@@ -2,7 +2,6 @@ import { record } from "@sailfish-rrweb/rrweb-record-only";
|
|
|
2
2
|
import { getRecordConsolePlugin, } from "@sailfish-rrweb/rrweb-plugin-console-record";
|
|
3
3
|
// import { NetworkRecordOptions } from "@sailfish-rrweb/rrweb-plugin-network-record";
|
|
4
4
|
import { EventType } from "@sailfish-rrweb/types";
|
|
5
|
-
import { ZendeskAPI } from "react-zendesk";
|
|
6
5
|
import { Complete, DomContentEventId, DomContentSource, Loading, } from "./constants";
|
|
7
6
|
import suppressConsoleLogsDuringCall from "./suppressConsoleLogsDuringCall";
|
|
8
7
|
import { initializeWebSocket, sendEvent } from "./websocket";
|
|
@@ -10,6 +9,53 @@ const MASK_CLASS = "sailfishSanitize";
|
|
|
10
9
|
const ZENDESK_ELEMENT_ID = "zendesk_chat";
|
|
11
10
|
const ZENDESK_PROVIDER = "Zendesk";
|
|
12
11
|
const ZENDESK_TAG_PREFIX = "sailfish-session-";
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// Zendesk (Messaging) optional helpers
|
|
14
|
+
// Docs (Messaging widget show/open + core API):
|
|
15
|
+
// - https://developer.zendesk.com/documentation/zendesk-web-widget-sdks/sdks/web/messaging-show-web-widget/
|
|
16
|
+
// - https://developer.zendesk.com/api-reference/widget-messaging/web/core/
|
|
17
|
+
// Conversation tags via Messaging:
|
|
18
|
+
// - zE('messenger:set', 'conversationTags', [...])
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
/** Is the Zendesk Messaging API available? */
|
|
21
|
+
function hasZendesk() {
|
|
22
|
+
return typeof window !== "undefined" && typeof window.zE === "function";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Queue-safe "ready" hook. If Zendesk is present, `zE(callback)` registers `callback` to run
|
|
26
|
+
* when the widget is ready (or queues it if loading). If absent, do nothing.
|
|
27
|
+
*/
|
|
28
|
+
function whenZendeskReady(ballback) {
|
|
29
|
+
if (typeof window === "undefined")
|
|
30
|
+
return;
|
|
31
|
+
const zE = window.zE;
|
|
32
|
+
if (typeof zE === "function") {
|
|
33
|
+
try {
|
|
34
|
+
// Per Zendesk snippet semantics, zE(ballback) queues the callback until ready.
|
|
35
|
+
zE(ballback);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Call zE safely. Returns true if the call executed. */
|
|
43
|
+
function zE_safe(...args) {
|
|
44
|
+
try {
|
|
45
|
+
if (hasZendesk()) {
|
|
46
|
+
// Non-null assertion ok: guarded by hasZendesk()
|
|
47
|
+
window.zE(...args);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// ignore
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
// Recorder logic
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
59
|
function maskInputFn(text, node) {
|
|
14
60
|
if (node.type === "hidden") {
|
|
15
61
|
return "";
|
|
@@ -122,37 +168,53 @@ backendApi, apiKey, sessionId) {
|
|
|
122
168
|
maskTextClass: MASK_CLASS,
|
|
123
169
|
...captureSettings,
|
|
124
170
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
171
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
172
|
+
// Zendesk (Messaging) — OPTIONAL
|
|
173
|
+
// If a site uses Zendesk Messaging (and includes the snippet), wire tags
|
|
174
|
+
// and events once the widget is ready. Otherwise, this is a no-op.
|
|
175
|
+
//
|
|
176
|
+
// APIs used (Messaging):
|
|
177
|
+
// zE('messenger:set', 'conversationTags', string[])
|
|
178
|
+
// zE('messenger:on', 'open' | 'close' | 'unreadMessages', callback)
|
|
179
|
+
// Docs: show/open guidance + core API.
|
|
180
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
181
|
+
whenZendeskReady(() => {
|
|
182
|
+
// Tag the conversation with our session id (if supported).
|
|
183
|
+
suppressConsoleLogsDuringCall(() => {
|
|
184
|
+
zE_safe("messenger:set", "conversationTags", [
|
|
185
|
+
`${ZENDESK_TAG_PREFIX}${sessionId}`,
|
|
186
|
+
]);
|
|
135
187
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
188
|
+
const handleWidgetOpen = () => {
|
|
189
|
+
record.addSailfishEvent(EventType.SailfishCustom, {
|
|
190
|
+
action: "customer support chat opened",
|
|
191
|
+
element_id: ZENDESK_ELEMENT_ID,
|
|
192
|
+
provider: ZENDESK_PROVIDER,
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
const handleWidgetClose = () => {
|
|
196
|
+
record.addSailfishEvent(EventType.SailfishCustom, {
|
|
197
|
+
action: "customer support chat closed",
|
|
198
|
+
element_id: ZENDESK_ELEMENT_ID,
|
|
199
|
+
provider: ZENDESK_PROVIDER,
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const handleUnreadMessages = (count) => {
|
|
203
|
+
record.addSailfishEvent(EventType.SailfishCustom, {
|
|
204
|
+
action: "zendesk unreadmessages",
|
|
205
|
+
element_id: ZENDESK_ELEMENT_ID,
|
|
206
|
+
provider: ZENDESK_PROVIDER,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
suppressConsoleLogsDuringCall(() => {
|
|
210
|
+
zE_safe("messenger:on", "open", handleWidgetOpen);
|
|
211
|
+
zE_safe("messenger:on", "close", handleWidgetClose);
|
|
212
|
+
zE_safe("messenger:on", "unreadMessages", handleUnreadMessages);
|
|
142
213
|
});
|
|
143
|
-
};
|
|
144
|
-
const handleUnreadMessages = (count) => {
|
|
145
|
-
record.addSailfishEvent(EventType.SailfishCustom, {
|
|
146
|
-
action: "zendesk unreadmessages",
|
|
147
|
-
element_id: ZENDESK_ELEMENT_ID,
|
|
148
|
-
provider: ZENDESK_PROVIDER,
|
|
149
|
-
});
|
|
150
|
-
};
|
|
151
|
-
suppressConsoleLogsDuringCall(() => {
|
|
152
|
-
ZendeskAPI("messenger:on", "open", handleWidgetOpen);
|
|
153
|
-
ZendeskAPI("messenger:on", "close", handleWidgetClose);
|
|
154
|
-
ZendeskAPI("messenger:on", "unreadMessages", handleUnreadMessages);
|
|
155
214
|
});
|
|
215
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
216
|
+
// End Zendesk (optional)
|
|
217
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
156
218
|
}
|
|
157
219
|
catch (error) {
|
|
158
220
|
console.error("Error importing plugins!", error);
|