@chaja/sdk-browser 0.0.0-stage → 0.1.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/README.md +22 -2
- package/dist/index.d.mts +130 -0
- package/dist/index.mjs +537 -0
- package/dist/replay.d.mts +61 -0
- package/dist/replay.mjs +146 -0
- package/package.json +57 -3
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @chaja/sdk-browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Error tracking for browsers, reporting to a self-hosted [Chajá](https://github.com/emiliodominguez/chaja) server.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add @chaja/sdk-browser
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { captureException, init } from "@chaja/sdk-browser";
|
|
11
|
+
|
|
12
|
+
init({ endpoint: "https://chaja.example.com", key: "<project ingest key>", release: "1.4.0", environment: "production" });
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await checkout();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
captureException(error, { tags: { step: "payment" } });
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Uncaught errors and unhandled rejections are captured automatically, with console, `fetch`, navigation and click breadcrumbs.
|
|
22
|
+
Upload sourcemaps with [`@chaja/cli`](https://www.npmjs.com/package/@chaja/cli) so minified frames show your source. All options:
|
|
23
|
+
[docs/sdk.md](https://github.com/emiliodominguez/chaja/blob/main/docs/sdk.md).
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { BreadcrumbInput, CaptureContext, CaptureContext as CaptureContext$1, CaptureOptions, CaptureOptions as CaptureOptions$1, Client, Client as Client$1, ClientOptions, ClientOptions as ClientOptions$1 } from "@chaja/sdk-core";
|
|
2
|
+
//#region src/index.d.ts
|
|
3
|
+
export interface BrowserOptions extends Omit<ClientOptions$1, "service"> {
|
|
4
|
+
/**
|
|
5
|
+
* Service name; defaults to the page host.
|
|
6
|
+
*/
|
|
7
|
+
service?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Which breadcrumbs to record automatically. All on by default.
|
|
10
|
+
*/
|
|
11
|
+
breadcrumbs?: {
|
|
12
|
+
console?: boolean;
|
|
13
|
+
fetch?: boolean;
|
|
14
|
+
navigation?: boolean;
|
|
15
|
+
clicks?: boolean;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Capture uncaught errors and unhandled promise rejections. Default true.
|
|
19
|
+
*/
|
|
20
|
+
globalHandlers?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Send a `$pageview` on load and on every client-side navigation to a new path. Default true.
|
|
23
|
+
*/
|
|
24
|
+
capturePageviews?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Load feature flags on start and after `identify` or `reset`. Default false.
|
|
27
|
+
*/
|
|
28
|
+
flags?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The window to instrument (tests pass a fake one).
|
|
31
|
+
*/
|
|
32
|
+
window?: Window & typeof globalThis;
|
|
33
|
+
}
|
|
34
|
+
export type FlagValue = boolean | string;
|
|
35
|
+
export interface BrowserClient extends Client$1 {
|
|
36
|
+
/**
|
|
37
|
+
* Remove every listener and wrapper this SDK installed.
|
|
38
|
+
*/
|
|
39
|
+
close(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Whether a flag is on for the current person (a variant counts as on); undefined until flags have loaded once.
|
|
42
|
+
*/
|
|
43
|
+
isFeatureEnabled(key: string): boolean | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* A flag's value: `false`, `true`, or the variant key; undefined until flags have loaded once.
|
|
46
|
+
*/
|
|
47
|
+
getFeatureFlag(key: string): FlagValue | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Called with every flag whenever they load; right away when they already have. Returns a function that stops it.
|
|
50
|
+
*/
|
|
51
|
+
onFeatureFlags(listener: (flags: Record<string, FlagValue>) => void): () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Fetch the current person's flags again.
|
|
54
|
+
*/
|
|
55
|
+
reloadFeatureFlags(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Start the browser SDK: capture uncaught errors and rejections, record breadcrumbs and send events to Chajá.
|
|
59
|
+
*
|
|
60
|
+
* @param options - SDK options.
|
|
61
|
+
* @returns The client; calling `init` again replaces it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function init(options: BrowserOptions): BrowserClient;
|
|
64
|
+
/**
|
|
65
|
+
* Capture an error with the client from `init`.
|
|
66
|
+
*
|
|
67
|
+
* @param error - Thrown value.
|
|
68
|
+
* @param context - Extra context.
|
|
69
|
+
* @returns Event id, if sent.
|
|
70
|
+
*/
|
|
71
|
+
export declare function captureException(error: unknown, context?: CaptureContext$1): string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Capture a message with the client from `init`.
|
|
74
|
+
*
|
|
75
|
+
* @param message - Message.
|
|
76
|
+
* @param level - Level.
|
|
77
|
+
* @param context - Extra context.
|
|
78
|
+
* @returns Event id, if sent.
|
|
79
|
+
*/
|
|
80
|
+
export declare function captureMessage(message: string, level?: Parameters<Client$1["captureMessage"]>[1], context?: CaptureContext$1): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* The client created by `init`, if any.
|
|
83
|
+
*
|
|
84
|
+
* @returns Current client.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getClient(): BrowserClient | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Record a product analytics event with the client from `init`.
|
|
89
|
+
*
|
|
90
|
+
* @param name - Event name, such as `signed_up`.
|
|
91
|
+
* @param properties - Event properties.
|
|
92
|
+
* @param options - Person override and person properties.
|
|
93
|
+
* @returns Event id, if sent.
|
|
94
|
+
*/
|
|
95
|
+
export declare function capture(name: string, properties?: Parameters<Client$1["capture"]>[1], options?: CaptureOptions$1): string | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Name the current person with your app's user id (after sign-in).
|
|
98
|
+
*
|
|
99
|
+
* @param distinctId - Your user id.
|
|
100
|
+
* @param set - Person properties, such as `{ email, plan }`.
|
|
101
|
+
* @param setOnce - Person properties to keep from the first time they are set.
|
|
102
|
+
*/
|
|
103
|
+
export declare function identify(distinctId: string, set?: Parameters<Client$1["identify"]>[1], setOnce?: Parameters<Client$1["identify"]>[2]): void;
|
|
104
|
+
/**
|
|
105
|
+
* Forget the current person (on sign-out).
|
|
106
|
+
*/
|
|
107
|
+
export declare function reset(): void;
|
|
108
|
+
/**
|
|
109
|
+
* Whether a flag is on for the current person, with the client from `init` (needs `flags: true`).
|
|
110
|
+
*
|
|
111
|
+
* @param key - Flag key.
|
|
112
|
+
* @returns On, off, or undefined before flags load.
|
|
113
|
+
*/
|
|
114
|
+
export declare function isFeatureEnabled(key: string): boolean | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* A flag's value for the current person, with the client from `init` (needs `flags: true`).
|
|
117
|
+
*
|
|
118
|
+
* @param key - Flag key.
|
|
119
|
+
* @returns `false`, `true`, the variant key, or undefined before flags load.
|
|
120
|
+
*/
|
|
121
|
+
export declare function getFeatureFlag(key: string): FlagValue | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Run a callback whenever flags load, with the client from `init`.
|
|
124
|
+
*
|
|
125
|
+
* @param listener - Called with every flag.
|
|
126
|
+
* @returns A function that stops it.
|
|
127
|
+
*/
|
|
128
|
+
export declare function onFeatureFlags(listener: (flags: Record<string, FlagValue>) => void): () => void;
|
|
129
|
+
//#endregion
|
|
130
|
+
export type { BreadcrumbInput, CaptureContext, CaptureOptions, Client, ClientOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { SDK_VERSION, createClient, parseRetryAfter, randomEventId } from "@chaja/sdk-core";
|
|
2
|
+
//#region src/index.ts
|
|
3
|
+
let current;
|
|
4
|
+
/**
|
|
5
|
+
* A session ends after this long without events, or after a day.
|
|
6
|
+
*/
|
|
7
|
+
const SESSION_IDLE_MS = 18e5;
|
|
8
|
+
const SESSION_MAX_MS = 864e5;
|
|
9
|
+
/**
|
|
10
|
+
* JSON state in localStorage that survives private modes and blocked storage by falling back to memory.
|
|
11
|
+
*
|
|
12
|
+
* @param view - Window.
|
|
13
|
+
* @param key - Storage key.
|
|
14
|
+
* @returns Read and write functions.
|
|
15
|
+
*/
|
|
16
|
+
function storedValue(view, key) {
|
|
17
|
+
let memory;
|
|
18
|
+
return {
|
|
19
|
+
read() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = view.localStorage.getItem(key);
|
|
22
|
+
return raw === null ? memory : JSON.parse(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
return memory;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
write(value) {
|
|
28
|
+
memory = value;
|
|
29
|
+
try {
|
|
30
|
+
if (value === void 0) view.localStorage.removeItem(key);
|
|
31
|
+
else view.localStorage.setItem(key, JSON.stringify(value));
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Narrow stored or fetched flags.
|
|
38
|
+
*
|
|
39
|
+
* @param value - Parsed JSON.
|
|
40
|
+
* @returns Flags, or undefined.
|
|
41
|
+
*/
|
|
42
|
+
function flagsOf(value) {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return;
|
|
44
|
+
const flags = {};
|
|
45
|
+
for (const [key, flag] of Object.entries(value)) if (typeof flag === "boolean" || typeof flag === "string") flags[key] = flag;
|
|
46
|
+
return flags;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Narrow stored identity.
|
|
50
|
+
*
|
|
51
|
+
* @param value - Parsed JSON.
|
|
52
|
+
* @returns Identity, or undefined.
|
|
53
|
+
*/
|
|
54
|
+
function identityOf(value) {
|
|
55
|
+
return typeof value === "object" && value !== null && "distinctId" in value && typeof value.distinctId === "string" && "identified" in value && typeof value.identified === "boolean" ? {
|
|
56
|
+
distinctId: value.distinctId,
|
|
57
|
+
identified: value.identified
|
|
58
|
+
} : void 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Narrow a stored session.
|
|
62
|
+
*
|
|
63
|
+
* @param value - Parsed JSON.
|
|
64
|
+
* @returns Session, or undefined.
|
|
65
|
+
*/
|
|
66
|
+
function sessionOf(value) {
|
|
67
|
+
return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && "startedAt" in value && typeof value.startedAt === "number" && "lastActivityAt" in value && typeof value.lastActivityAt === "number" ? {
|
|
68
|
+
id: value.id,
|
|
69
|
+
startedAt: value.startedAt,
|
|
70
|
+
lastActivityAt: value.lastActivityAt
|
|
71
|
+
} : void 0;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Describe a clicked element compactly: `button#save.primary`.
|
|
75
|
+
*
|
|
76
|
+
* @param target - Event target.
|
|
77
|
+
* @returns Selector-like text.
|
|
78
|
+
*/
|
|
79
|
+
function describeElement(target) {
|
|
80
|
+
if (!isElement(target)) return;
|
|
81
|
+
const element = target;
|
|
82
|
+
const id = element.id ? `#${element.id}` : "";
|
|
83
|
+
const classes = typeof element.className === "string" && element.className ? `.${element.className.trim().split(/\s+/u).slice(0, 3).join(".")}` : "";
|
|
84
|
+
const label = element.getAttribute("aria-label") ?? element.getAttribute("name");
|
|
85
|
+
return `${element.tagName.toLowerCase()}${id}${classes}${label ? `[${label.slice(0, 40)}]` : ""}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether an event target is a DOM element.
|
|
89
|
+
*
|
|
90
|
+
* @param target - Event target.
|
|
91
|
+
* @returns True for elements.
|
|
92
|
+
*/
|
|
93
|
+
function isElement(target) {
|
|
94
|
+
return typeof target === "object" && target !== null && "tagName" in target && typeof target.tagName === "string" && "getAttribute" in target;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Start the browser SDK: capture uncaught errors and rejections, record breadcrumbs and send events to Chajá.
|
|
98
|
+
*
|
|
99
|
+
* @param options - SDK options.
|
|
100
|
+
* @returns The client; calling `init` again replaces it.
|
|
101
|
+
*/
|
|
102
|
+
function init(options) {
|
|
103
|
+
current?.close();
|
|
104
|
+
const view = options.window ?? window;
|
|
105
|
+
const envelopeUrl = `${options.endpoint.replace(/\/+$/u, "")}/api/v1/envelope`;
|
|
106
|
+
const cleanups = [];
|
|
107
|
+
const nativeFetch = Reflect.get(view, "fetch");
|
|
108
|
+
const originalFetch = view.fetch.bind(view);
|
|
109
|
+
const transport = {
|
|
110
|
+
async send(body) {
|
|
111
|
+
try {
|
|
112
|
+
const response = await originalFetch(envelopeUrl, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
"content-type": "application/json",
|
|
116
|
+
"x-chaja-key": options.key
|
|
117
|
+
},
|
|
118
|
+
body,
|
|
119
|
+
keepalive: body.length < 6e4
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
status: response.status,
|
|
123
|
+
...response.status === 429 || response.status === 503 ? optionalRetry(response.headers.get("retry-after")) : {}
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
return { status: 0 };
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
sendFinal(body) {
|
|
130
|
+
const navigatorRef = view.navigator;
|
|
131
|
+
return typeof navigatorRef.sendBeacon === "function" && navigatorRef.sendBeacon(`${envelopeUrl}?key=${encodeURIComponent(options.key)}`, new Blob([body], { type: "text/plain" }));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const identityStore = storedValue(view, `chaja_id_${options.key}`);
|
|
135
|
+
const sessionStore = storedValue(view, `chaja_session_${options.key}`);
|
|
136
|
+
const flagStore = storedValue(view, `chaja_flags_${options.key}`);
|
|
137
|
+
const flagsUrl = `${options.endpoint.replace(/\/+$/u, "")}/api/v1/flags`;
|
|
138
|
+
const flagListeners = /* @__PURE__ */ new Set();
|
|
139
|
+
const reported = /* @__PURE__ */ new Set();
|
|
140
|
+
let flags = options.flags ? flagsOf(flagStore.read()) : void 0;
|
|
141
|
+
const client = createClient({
|
|
142
|
+
...options,
|
|
143
|
+
service: options.service ?? view.location.host
|
|
144
|
+
}, {
|
|
145
|
+
sdk: {
|
|
146
|
+
name: "chaja.javascript.browser",
|
|
147
|
+
version: SDK_VERSION
|
|
148
|
+
},
|
|
149
|
+
platform: "javascript",
|
|
150
|
+
transport,
|
|
151
|
+
eventId: randomEventId,
|
|
152
|
+
now: Date.now,
|
|
153
|
+
random: Math.random,
|
|
154
|
+
schedule(callback, delayMs) {
|
|
155
|
+
return view.setTimeout(callback, delayMs);
|
|
156
|
+
},
|
|
157
|
+
cancel(handle) {
|
|
158
|
+
if (typeof handle === "number") view.clearTimeout(handle);
|
|
159
|
+
},
|
|
160
|
+
enrich(item) {
|
|
161
|
+
return {
|
|
162
|
+
...item,
|
|
163
|
+
request: {
|
|
164
|
+
url: view.location.href,
|
|
165
|
+
headers: { "user-agent": view.navigator.userAgent },
|
|
166
|
+
...item.request
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
identity: {
|
|
171
|
+
load() {
|
|
172
|
+
return identityOf(identityStore.read());
|
|
173
|
+
},
|
|
174
|
+
save(state) {
|
|
175
|
+
identityStore.write(state);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
session: {
|
|
179
|
+
current() {
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const stored = sessionOf(sessionStore.read());
|
|
182
|
+
const session = stored && now - stored.lastActivityAt < SESSION_IDLE_MS && now - stored.startedAt < SESSION_MAX_MS ? {
|
|
183
|
+
...stored,
|
|
184
|
+
lastActivityAt: now
|
|
185
|
+
} : {
|
|
186
|
+
id: randomEventId(),
|
|
187
|
+
startedAt: now,
|
|
188
|
+
lastActivityAt: now
|
|
189
|
+
};
|
|
190
|
+
sessionStore.write(session);
|
|
191
|
+
return session.id;
|
|
192
|
+
},
|
|
193
|
+
reset() {
|
|
194
|
+
sessionStore.write(void 0);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
eventProperties() {
|
|
198
|
+
return {
|
|
199
|
+
$current_url: view.location.href,
|
|
200
|
+
$host: view.location.host,
|
|
201
|
+
$pathname: view.location.pathname,
|
|
202
|
+
$referrer: view.document.referrer,
|
|
203
|
+
$screen_width: view.screen.width,
|
|
204
|
+
$screen_height: view.screen.height,
|
|
205
|
+
$viewport_width: view.innerWidth,
|
|
206
|
+
$viewport_height: view.innerHeight,
|
|
207
|
+
$lib: "chaja.javascript.browser",
|
|
208
|
+
$lib_version: SDK_VERSION,
|
|
209
|
+
...flagProperties()
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
let lastPage;
|
|
214
|
+
/**
|
|
215
|
+
* The loaded flags as `$feature/<key>` event properties, so Insights can compare people by flag.
|
|
216
|
+
*
|
|
217
|
+
* @returns Properties.
|
|
218
|
+
*/
|
|
219
|
+
function flagProperties() {
|
|
220
|
+
const properties = {};
|
|
221
|
+
for (const [key, value] of Object.entries(flags ?? {})) properties[`$feature/${key}`] = value;
|
|
222
|
+
return properties;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Fetch the current person's flags, keep them for the next page load and tell listeners.
|
|
226
|
+
*/
|
|
227
|
+
async function reloadFeatureFlags() {
|
|
228
|
+
try {
|
|
229
|
+
const response = await originalFetch(flagsUrl, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: {
|
|
232
|
+
"content-type": "application/json",
|
|
233
|
+
"x-chaja-key": options.key
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify({ distinctId: client.getDistinctId() })
|
|
236
|
+
});
|
|
237
|
+
const body = response.ok ? await response.json() : void 0;
|
|
238
|
+
const loaded = typeof body === "object" && body !== null && "flags" in body ? flagsOf(body.flags) : void 0;
|
|
239
|
+
if (!loaded) return;
|
|
240
|
+
flags = loaded;
|
|
241
|
+
flagStore.write(loaded);
|
|
242
|
+
for (const listener of flagListeners) listener(loaded);
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Read a flag, recording the first read of each value as `$feature_flag_called`.
|
|
247
|
+
*
|
|
248
|
+
* @param key - Flag key.
|
|
249
|
+
* @returns Value, if loaded.
|
|
250
|
+
*/
|
|
251
|
+
function readFlag(key) {
|
|
252
|
+
const value = flags?.[key];
|
|
253
|
+
if (value !== void 0 && !reported.has(`${key}:${String(value)}`)) {
|
|
254
|
+
reported.add(`${key}:${String(value)}`);
|
|
255
|
+
client.capture("$feature_flag_called", {
|
|
256
|
+
$feature_flag: key,
|
|
257
|
+
$feature_flag_response: value
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Send a page view when the path or query changed since the last one.
|
|
264
|
+
*/
|
|
265
|
+
function pageview() {
|
|
266
|
+
const page = `${view.location.pathname}${view.location.search}`;
|
|
267
|
+
if (options.capturePageviews !== false && page !== lastPage) {
|
|
268
|
+
lastPage = page;
|
|
269
|
+
client.capture("$pageview");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const crumbs = options.breadcrumbs ?? {};
|
|
273
|
+
/**
|
|
274
|
+
* Listen to a window event and remember to remove the listener.
|
|
275
|
+
*
|
|
276
|
+
* @param type - Event type.
|
|
277
|
+
* @param listener - Listener.
|
|
278
|
+
* @param useCapture - Use the capture phase.
|
|
279
|
+
*/
|
|
280
|
+
function listen(type, listener, useCapture = false) {
|
|
281
|
+
view.addEventListener(type, listener, useCapture);
|
|
282
|
+
cleanups.push(function remove() {
|
|
283
|
+
view.removeEventListener(type, listener, useCapture);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (options.globalHandlers !== false) {
|
|
287
|
+
listen("error", function onError(event) {
|
|
288
|
+
const error = event.error ?? new Error(event.message || "Script error");
|
|
289
|
+
client.captureException(error, { mechanism: {
|
|
290
|
+
type: "onerror",
|
|
291
|
+
handled: false
|
|
292
|
+
} });
|
|
293
|
+
});
|
|
294
|
+
listen("unhandledrejection", function onRejection(event) {
|
|
295
|
+
client.captureException(event.reason, { mechanism: {
|
|
296
|
+
type: "onunhandledrejection",
|
|
297
|
+
handled: false
|
|
298
|
+
} });
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
listen("pagehide", function onPageHide() {
|
|
302
|
+
client.sendFinal();
|
|
303
|
+
});
|
|
304
|
+
if (crumbs.console !== false) for (const method of [
|
|
305
|
+
"warn",
|
|
306
|
+
"error",
|
|
307
|
+
"info",
|
|
308
|
+
"log"
|
|
309
|
+
]) {
|
|
310
|
+
const original = view.console[method];
|
|
311
|
+
view.console[method] = function patched(...args) {
|
|
312
|
+
client.addBreadcrumb({
|
|
313
|
+
category: "console",
|
|
314
|
+
level: method === "warn" ? "warning" : method === "error" ? "error" : "info",
|
|
315
|
+
message: args.map(function text(arg) {
|
|
316
|
+
return typeof arg === "string" ? arg : arg instanceof Error ? `${arg.name}: ${arg.message}` : safeJson(arg);
|
|
317
|
+
}).join(" ").slice(0, 1e3)
|
|
318
|
+
});
|
|
319
|
+
original.apply(view.console, args);
|
|
320
|
+
};
|
|
321
|
+
cleanups.push(function restore() {
|
|
322
|
+
view.console[method] = original;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (crumbs.fetch !== false) {
|
|
326
|
+
view.fetch = async function trackedFetch(input, requestInit) {
|
|
327
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
328
|
+
const method = (requestInit?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
329
|
+
const started = Date.now();
|
|
330
|
+
if (url.startsWith(envelopeUrl)) return originalFetch(input, requestInit);
|
|
331
|
+
try {
|
|
332
|
+
const response = await originalFetch(input, requestInit);
|
|
333
|
+
client.addBreadcrumb({
|
|
334
|
+
category: "fetch",
|
|
335
|
+
level: response.status >= 500 ? "error" : response.status >= 400 ? "warning" : "info",
|
|
336
|
+
message: `${method} ${url}`,
|
|
337
|
+
data: {
|
|
338
|
+
status: response.status,
|
|
339
|
+
durationMs: Date.now() - started
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return response;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
client.addBreadcrumb({
|
|
345
|
+
category: "fetch",
|
|
346
|
+
level: "error",
|
|
347
|
+
message: `${method} ${url}`,
|
|
348
|
+
data: {
|
|
349
|
+
failed: true,
|
|
350
|
+
durationMs: Date.now() - started
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
cleanups.push(function restore() {
|
|
357
|
+
Reflect.set(view, "fetch", nativeFetch);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
let last = view.location.href;
|
|
361
|
+
/**
|
|
362
|
+
* Record a URL change as a breadcrumb.
|
|
363
|
+
*/
|
|
364
|
+
function navigated() {
|
|
365
|
+
const next = view.location.href;
|
|
366
|
+
if (crumbs.navigation !== false && next !== last) client.addBreadcrumb({
|
|
367
|
+
category: "navigation",
|
|
368
|
+
message: `${last} → ${next}`
|
|
369
|
+
});
|
|
370
|
+
last = next;
|
|
371
|
+
}
|
|
372
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
373
|
+
const original = view.history[method];
|
|
374
|
+
view.history[method] = function patched(...args) {
|
|
375
|
+
const result = original.apply(this, args);
|
|
376
|
+
navigated();
|
|
377
|
+
pageview();
|
|
378
|
+
return result;
|
|
379
|
+
};
|
|
380
|
+
cleanups.push(function restore() {
|
|
381
|
+
view.history[method] = original;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
listen("popstate", function onPopState() {
|
|
385
|
+
navigated();
|
|
386
|
+
pageview();
|
|
387
|
+
});
|
|
388
|
+
if (crumbs.clicks !== false) listen("click", function onClick(event) {
|
|
389
|
+
const target = describeElement(event.target);
|
|
390
|
+
if (target) client.addBreadcrumb({
|
|
391
|
+
category: "ui.click",
|
|
392
|
+
message: target
|
|
393
|
+
});
|
|
394
|
+
}, true);
|
|
395
|
+
const instance = {
|
|
396
|
+
...client,
|
|
397
|
+
identify(distinctId, set, setOnce) {
|
|
398
|
+
const before = client.getDistinctId();
|
|
399
|
+
client.identify(distinctId, set, setOnce);
|
|
400
|
+
if (options.flags && client.getDistinctId() !== before) reloadFeatureFlags();
|
|
401
|
+
},
|
|
402
|
+
reset() {
|
|
403
|
+
client.reset();
|
|
404
|
+
reported.clear();
|
|
405
|
+
if (options.flags) reloadFeatureFlags();
|
|
406
|
+
},
|
|
407
|
+
isFeatureEnabled(key) {
|
|
408
|
+
const value = readFlag(key);
|
|
409
|
+
return value === void 0 ? void 0 : value !== false;
|
|
410
|
+
},
|
|
411
|
+
getFeatureFlag: readFlag,
|
|
412
|
+
onFeatureFlags(listener) {
|
|
413
|
+
flagListeners.add(listener);
|
|
414
|
+
if (flags) listener(flags);
|
|
415
|
+
return function stop() {
|
|
416
|
+
flagListeners.delete(listener);
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
reloadFeatureFlags,
|
|
420
|
+
close() {
|
|
421
|
+
for (const cleanup of cleanups.splice(0)) cleanup();
|
|
422
|
+
if (current === instance) current = void 0;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
current = instance;
|
|
426
|
+
pageview();
|
|
427
|
+
if (options.flags) reloadFeatureFlags();
|
|
428
|
+
return instance;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Build the optional retry-after field.
|
|
432
|
+
*
|
|
433
|
+
* @param header - Retry-After header.
|
|
434
|
+
* @returns Field to spread into a transport result.
|
|
435
|
+
*/
|
|
436
|
+
function optionalRetry(header) {
|
|
437
|
+
const seconds = parseRetryAfter(header);
|
|
438
|
+
return seconds === void 0 ? {} : { retryAfterSeconds: seconds };
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* JSON for console breadcrumbs, tolerating cycles.
|
|
442
|
+
*
|
|
443
|
+
* @param value - Any value.
|
|
444
|
+
* @returns Text.
|
|
445
|
+
*/
|
|
446
|
+
function safeJson(value) {
|
|
447
|
+
try {
|
|
448
|
+
return JSON.stringify(value) ?? String(value);
|
|
449
|
+
} catch {
|
|
450
|
+
return String(value);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Capture an error with the client from `init`.
|
|
455
|
+
*
|
|
456
|
+
* @param error - Thrown value.
|
|
457
|
+
* @param context - Extra context.
|
|
458
|
+
* @returns Event id, if sent.
|
|
459
|
+
*/
|
|
460
|
+
function captureException(error, context) {
|
|
461
|
+
return current?.captureException(error, context);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Capture a message with the client from `init`.
|
|
465
|
+
*
|
|
466
|
+
* @param message - Message.
|
|
467
|
+
* @param level - Level.
|
|
468
|
+
* @param context - Extra context.
|
|
469
|
+
* @returns Event id, if sent.
|
|
470
|
+
*/
|
|
471
|
+
function captureMessage(message, level, context) {
|
|
472
|
+
return current?.captureMessage(message, level, context);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* The client created by `init`, if any.
|
|
476
|
+
*
|
|
477
|
+
* @returns Current client.
|
|
478
|
+
*/
|
|
479
|
+
function getClient() {
|
|
480
|
+
return current;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Record a product analytics event with the client from `init`.
|
|
484
|
+
*
|
|
485
|
+
* @param name - Event name, such as `signed_up`.
|
|
486
|
+
* @param properties - Event properties.
|
|
487
|
+
* @param options - Person override and person properties.
|
|
488
|
+
* @returns Event id, if sent.
|
|
489
|
+
*/
|
|
490
|
+
function capture(name, properties, options) {
|
|
491
|
+
return current?.capture(name, properties, options);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Name the current person with your app's user id (after sign-in).
|
|
495
|
+
*
|
|
496
|
+
* @param distinctId - Your user id.
|
|
497
|
+
* @param set - Person properties, such as `{ email, plan }`.
|
|
498
|
+
* @param setOnce - Person properties to keep from the first time they are set.
|
|
499
|
+
*/
|
|
500
|
+
function identify(distinctId, set, setOnce) {
|
|
501
|
+
current?.identify(distinctId, set, setOnce);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Forget the current person (on sign-out).
|
|
505
|
+
*/
|
|
506
|
+
function reset() {
|
|
507
|
+
current?.reset();
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Whether a flag is on for the current person, with the client from `init` (needs `flags: true`).
|
|
511
|
+
*
|
|
512
|
+
* @param key - Flag key.
|
|
513
|
+
* @returns On, off, or undefined before flags load.
|
|
514
|
+
*/
|
|
515
|
+
function isFeatureEnabled(key) {
|
|
516
|
+
return current?.isFeatureEnabled(key);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* A flag's value for the current person, with the client from `init` (needs `flags: true`).
|
|
520
|
+
*
|
|
521
|
+
* @param key - Flag key.
|
|
522
|
+
* @returns `false`, `true`, the variant key, or undefined before flags load.
|
|
523
|
+
*/
|
|
524
|
+
function getFeatureFlag(key) {
|
|
525
|
+
return current?.getFeatureFlag(key);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Run a callback whenever flags load, with the client from `init`.
|
|
529
|
+
*
|
|
530
|
+
* @param listener - Called with every flag.
|
|
531
|
+
* @returns A function that stops it.
|
|
532
|
+
*/
|
|
533
|
+
function onFeatureFlags(listener) {
|
|
534
|
+
return current?.onFeatureFlags(listener) ?? function nothing() {};
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
export { capture, captureException, captureMessage, getClient, getFeatureFlag, identify, init, isFeatureEnabled, onFeatureFlags, reset };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/replay.d.ts
|
|
2
|
+
export interface ReplayOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Share of sessions recorded, decided once per session. Default 1.
|
|
5
|
+
*/
|
|
6
|
+
sampleRate?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Replace what people type with asterisks. Default true.
|
|
9
|
+
*/
|
|
10
|
+
maskAllInputs?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Elements whose text is masked, such as `[data-private]`.
|
|
13
|
+
*/
|
|
14
|
+
maskTextSelector?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Elements left out of the recording entirely. Default `.chaja-block`.
|
|
17
|
+
*/
|
|
18
|
+
blockSelector?: string;
|
|
19
|
+
/**
|
|
20
|
+
* How often recorded events are sent. Default 5 seconds.
|
|
21
|
+
*/
|
|
22
|
+
flushIntervalMs?: number;
|
|
23
|
+
/**
|
|
24
|
+
* The window to record (tests pass a fake one).
|
|
25
|
+
*/
|
|
26
|
+
window?: Window & typeof globalThis;
|
|
27
|
+
/**
|
|
28
|
+
* The recorder (tests pass a fake one).
|
|
29
|
+
*/
|
|
30
|
+
recorder?: Recorder;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The part of rrweb's `record` the replay uses.
|
|
34
|
+
*/
|
|
35
|
+
export interface Recorder {
|
|
36
|
+
(options: {
|
|
37
|
+
emit: (event: unknown) => void;
|
|
38
|
+
maskAllInputs?: boolean;
|
|
39
|
+
maskTextSelector?: string;
|
|
40
|
+
blockSelector?: string;
|
|
41
|
+
recordCanvas?: boolean;
|
|
42
|
+
inlineStylesheet?: boolean;
|
|
43
|
+
}): (() => void) | undefined;
|
|
44
|
+
takeFullSnapshot?: (isCheckout?: boolean) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Record the page as a session replay with the client from `init`: the DOM and what people do with it, sent in chunks to
|
|
48
|
+
* Chajá under the analytics session, so a replay sits next to that session's events and errors. Inputs are masked by
|
|
49
|
+
* default; add `chaja-block` to an element to leave it out.
|
|
50
|
+
*
|
|
51
|
+
* @param options - Sampling, privacy and timing.
|
|
52
|
+
* @returns A function that stops recording and sends what is left.
|
|
53
|
+
* @example
|
|
54
|
+
* import { init } from "@chaja/sdk-browser";
|
|
55
|
+
* import { startReplay } from "@chaja/sdk-browser/replay";
|
|
56
|
+
*
|
|
57
|
+
* init({ endpoint, key });
|
|
58
|
+
* startReplay({ sampleRate: 0.5 });
|
|
59
|
+
*/
|
|
60
|
+
export declare function startReplay(options?: ReplayOptions): () => void;
|
|
61
|
+
//#endregion
|
package/dist/replay.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { getClient } from "./index.mjs";
|
|
2
|
+
import { record } from "@rrweb/record";
|
|
3
|
+
//#region src/replay.ts
|
|
4
|
+
const MAX_BUFFER = 500;
|
|
5
|
+
const KEEPALIVE_LIMIT = 6e4;
|
|
6
|
+
/**
|
|
7
|
+
* Compress a body with gzip where the browser can.
|
|
8
|
+
*
|
|
9
|
+
* @param view - Window.
|
|
10
|
+
* @param body - JSON text.
|
|
11
|
+
* @returns Body and its encoding.
|
|
12
|
+
*/
|
|
13
|
+
async function compress(view, body) {
|
|
14
|
+
if (typeof view.CompressionStream !== "function") return {
|
|
15
|
+
body,
|
|
16
|
+
gzip: false
|
|
17
|
+
};
|
|
18
|
+
const stream = new Blob([body]).stream().pipeThrough(new view.CompressionStream("gzip"));
|
|
19
|
+
return {
|
|
20
|
+
body: await new Response(stream).arrayBuffer(),
|
|
21
|
+
gzip: true
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Record the page as a session replay with the client from `init`: the DOM and what people do with it, sent in chunks to
|
|
26
|
+
* Chajá under the analytics session, so a replay sits next to that session's events and errors. Inputs are masked by
|
|
27
|
+
* default; add `chaja-block` to an element to leave it out.
|
|
28
|
+
*
|
|
29
|
+
* @param options - Sampling, privacy and timing.
|
|
30
|
+
* @returns A function that stops recording and sends what is left.
|
|
31
|
+
* @example
|
|
32
|
+
* import { init } from "@chaja/sdk-browser";
|
|
33
|
+
* import { startReplay } from "@chaja/sdk-browser/replay";
|
|
34
|
+
*
|
|
35
|
+
* init({ endpoint, key });
|
|
36
|
+
* startReplay({ sampleRate: 0.5 });
|
|
37
|
+
*/
|
|
38
|
+
function startReplay(options = {}) {
|
|
39
|
+
const client = getClient();
|
|
40
|
+
if (!client) return function nothing() {};
|
|
41
|
+
const view = options.window ?? window;
|
|
42
|
+
const recorder = options.recorder ?? record;
|
|
43
|
+
const url = `${client.options.endpoint.replace(/\/+$/u, "")}/api/v1/replay`;
|
|
44
|
+
const sampleRate = options.sampleRate ?? 1;
|
|
45
|
+
const state = {
|
|
46
|
+
session: client.getSessionId() ?? "",
|
|
47
|
+
seq: 0,
|
|
48
|
+
sampled: false,
|
|
49
|
+
buffer: []
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Whether this session is recorded, decided once and kept for the tab.
|
|
53
|
+
*
|
|
54
|
+
* @param session - Session id.
|
|
55
|
+
* @returns True when recorded.
|
|
56
|
+
*/
|
|
57
|
+
function sampled(session) {
|
|
58
|
+
const key = `chaja_replay_${session}`;
|
|
59
|
+
try {
|
|
60
|
+
const stored = view.sessionStorage.getItem(key);
|
|
61
|
+
if (stored !== null) return stored === "1";
|
|
62
|
+
const decision = Math.random() < sampleRate;
|
|
63
|
+
view.sessionStorage.setItem(key, decision ? "1" : "0");
|
|
64
|
+
return decision;
|
|
65
|
+
} catch {
|
|
66
|
+
return Math.random() < sampleRate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
state.sampled = sampled(state.session);
|
|
70
|
+
/**
|
|
71
|
+
* Send buffered events as the next chunk of their session.
|
|
72
|
+
*
|
|
73
|
+
* @param final - The page is going away: keep the request alive.
|
|
74
|
+
*/
|
|
75
|
+
async function flush(final = false) {
|
|
76
|
+
const events = state.buffer.splice(0);
|
|
77
|
+
if (events.length === 0 || !state.sampled || !client) return;
|
|
78
|
+
const chunk = JSON.stringify({
|
|
79
|
+
sessionId: state.session,
|
|
80
|
+
distinctId: client.getDistinctId(),
|
|
81
|
+
seq: state.seq,
|
|
82
|
+
events
|
|
83
|
+
});
|
|
84
|
+
state.seq += 1;
|
|
85
|
+
try {
|
|
86
|
+
const payload = final ? {
|
|
87
|
+
body: chunk,
|
|
88
|
+
gzip: false
|
|
89
|
+
} : await compress(view, chunk);
|
|
90
|
+
await view.fetch(url, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
"x-chaja-key": client.options.key,
|
|
95
|
+
...payload.gzip ? { "content-encoding": "gzip" } : {}
|
|
96
|
+
},
|
|
97
|
+
body: payload.body,
|
|
98
|
+
keepalive: final && chunk.length < KEEPALIVE_LIMIT
|
|
99
|
+
});
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Flush, first moving to a new session when the old one expired, with a full snapshot so the new replay starts whole.
|
|
104
|
+
*/
|
|
105
|
+
function tick() {
|
|
106
|
+
if (!client) return;
|
|
107
|
+
const session = client.getSessionId() ?? state.session;
|
|
108
|
+
if (session === state.session) {
|
|
109
|
+
flush();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
flush();
|
|
113
|
+
state.session = session;
|
|
114
|
+
state.seq = 0;
|
|
115
|
+
state.sampled = sampled(session);
|
|
116
|
+
recorder.takeFullSnapshot?.(true);
|
|
117
|
+
}
|
|
118
|
+
const stopRecording = recorder({
|
|
119
|
+
emit(event) {
|
|
120
|
+
if (!state.sampled) return;
|
|
121
|
+
state.buffer.push(event);
|
|
122
|
+
if (state.buffer.length >= MAX_BUFFER) flush();
|
|
123
|
+
},
|
|
124
|
+
maskAllInputs: options.maskAllInputs ?? true,
|
|
125
|
+
blockSelector: options.blockSelector ?? ".chaja-block",
|
|
126
|
+
...options.maskTextSelector ? { maskTextSelector: options.maskTextSelector } : {},
|
|
127
|
+
recordCanvas: false,
|
|
128
|
+
inlineStylesheet: true
|
|
129
|
+
});
|
|
130
|
+
const timer = view.setInterval(tick, options.flushIntervalMs ?? 5e3);
|
|
131
|
+
/**
|
|
132
|
+
* Send what is left when the page is hidden.
|
|
133
|
+
*/
|
|
134
|
+
function onHide() {
|
|
135
|
+
flush(true);
|
|
136
|
+
}
|
|
137
|
+
view.addEventListener("pagehide", onHide);
|
|
138
|
+
return function stop() {
|
|
139
|
+
stopRecording?.();
|
|
140
|
+
view.clearInterval(timer);
|
|
141
|
+
view.removeEventListener("pagehide", onHide);
|
|
142
|
+
flush(true);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { startReplay };
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaja/sdk-browser",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Chajá error tracking for browsers: global handlers, breadcrumbs and sendBeacon delivery.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chaja",
|
|
7
|
+
"error-tracking",
|
|
8
|
+
"observability",
|
|
9
|
+
"browser",
|
|
10
|
+
"sdk"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/emiliodominguez/chaja#readme",
|
|
13
|
+
"bugs": "https://github.com/emiliodominguez/chaja/issues",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/emiliodominguez/chaja.git",
|
|
17
|
+
"directory": "packages/sdk-browser"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"nx": {
|
|
21
|
+
"tags": [
|
|
22
|
+
"scope:sdk",
|
|
23
|
+
"type:sdk"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./replay": {
|
|
32
|
+
"types": "./dist/replay.d.mts",
|
|
33
|
+
"default": "./dist/replay.mjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@rrweb/record": "^2.1.7"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@rrweb/record": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@chaja/protocol": "0.0.0",
|
|
50
|
+
"@rrweb/record": "2.1.7",
|
|
51
|
+
"tsdown": "0.23.0"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@chaja/sdk-core": "0.1.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsdown src/index.ts src/replay.ts --format esm --dts --clean --out-dir dist",
|
|
58
|
+
"typecheck": "tsc --noEmit -p ."
|
|
59
|
+
}
|
|
6
60
|
}
|