@heystack/otel 0.17.0 → 0.19.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 +169 -26
- package/dist/core.d.ts +1 -0
- package/dist/core.js +4 -0
- package/dist/hash.d.ts +19 -0
- package/dist/hash.js +47 -0
- package/dist/next.d.ts +2 -1
- package/dist/next.js +5 -0
- package/dist/node.d.ts +14 -2
- package/dist/node.js +49 -4
- package/dist/product.d.ts +36 -0
- package/dist/product.js +41 -3
- package/dist/redact.d.ts +57 -0
- package/dist/redact.js +190 -0
- package/dist/web.d.ts +42 -0
- package/dist/web.js +91 -24
- package/dist/workers-bindings.d.ts +39 -2
- package/dist/workers-bindings.js +351 -76
- package/dist/workers-coverage.d.ts +11 -0
- package/dist/workers-coverage.js +22 -0
- package/dist/workers-native.d.ts +66 -0
- package/dist/workers-native.js +147 -0
- package/dist/workers-propagation.d.ts +24 -0
- package/dist/workers-propagation.js +72 -0
- package/dist/workers-routes.d.ts +11 -0
- package/dist/workers-routes.js +45 -0
- package/dist/workers-sampler.d.ts +10 -1
- package/dist/workers-sampler.js +7 -3
- package/dist/workers.d.ts +108 -19
- package/dist/workers.js +583 -185
- package/package.json +9 -4
package/dist/product.d.ts
CHANGED
|
@@ -2,6 +2,19 @@ import { type HeystackOptions } from "./core.js";
|
|
|
2
2
|
export interface ActivityContext {
|
|
3
3
|
/** Stable pseudonymous identifiers; never email addresses or credentials. */
|
|
4
4
|
userId?: string;
|
|
5
|
+
/** Optional display context, exported only with allowUserProfiles. */
|
|
6
|
+
userProfile?: {
|
|
7
|
+
displayName: string;
|
|
8
|
+
source: "app" | "clerk";
|
|
9
|
+
};
|
|
10
|
+
/** Observed client network context, never inferred from an exporter server. */
|
|
11
|
+
network?: {
|
|
12
|
+
ip?: string;
|
|
13
|
+
country?: string;
|
|
14
|
+
region?: string;
|
|
15
|
+
city?: string;
|
|
16
|
+
source?: string;
|
|
17
|
+
};
|
|
5
18
|
sessionId?: string;
|
|
6
19
|
feature?: string;
|
|
7
20
|
/** Stable screen/route name, not a URL containing customer data. */
|
|
@@ -11,12 +24,20 @@ export interface ActivityContext {
|
|
|
11
24
|
}
|
|
12
25
|
export interface ProductEvent extends ActivityContext {
|
|
13
26
|
name: string;
|
|
27
|
+
eventId?: string;
|
|
28
|
+
attemptId?: string;
|
|
14
29
|
action?: string;
|
|
15
30
|
outcome?: "started" | "succeeded" | "failed" | "cancelled";
|
|
16
31
|
properties?: Record<string, string | number | boolean>;
|
|
17
32
|
}
|
|
18
33
|
export interface ProductAnalyticsOptions extends HeystackOptions {
|
|
19
34
|
environment?: string;
|
|
35
|
+
allowUserProfiles?: boolean;
|
|
36
|
+
allowNetworkContext?: boolean;
|
|
37
|
+
/** Optional versioned event catalogue: rejects unlisted names or missing context. */
|
|
38
|
+
eventDefinitions?: Readonly<Record<string, {
|
|
39
|
+
required?: readonly ("feature" | "surface" | "sessionId" | "userId" | "attemptId" | "outcome")[];
|
|
40
|
+
}>>;
|
|
20
41
|
/** Supply an original, uninstrumented fetch if browser fetch is already patched. */
|
|
21
42
|
fetch?: typeof fetch;
|
|
22
43
|
/** Explicitly allow metadata keys. Unlisted properties are discarded. */
|
|
@@ -39,3 +60,18 @@ export interface ProductAnalytics extends ActivityTracker {
|
|
|
39
60
|
* Bounded, best-effort collection; never auto-captures UI text or form contents.
|
|
40
61
|
* The return value means queued, not delivered or verified business success. */
|
|
41
62
|
export declare function createProductAnalytics(options: ProductAnalyticsOptions): ProductAnalytics;
|
|
63
|
+
/** Per-browser-session facade. Do not share this mutable identity facade on servers. */
|
|
64
|
+
export declare function createBrowserProductAnalytics(options: ProductAnalyticsOptions & {
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
}): {
|
|
67
|
+
readonly sessionId: string;
|
|
68
|
+
/** Starts a fresh session on identity changes. Reconfigure replay with returned ID. */
|
|
69
|
+
identify(userId: string, profile?: ActivityContext["userProfile"]): string;
|
|
70
|
+
/** Logout boundary: clear identity and rotate session. Reconfigure replay too. */
|
|
71
|
+
reset(): string;
|
|
72
|
+
track(event: ProductEvent): boolean;
|
|
73
|
+
screen(surface: string, event?: Omit<ProductEvent, "name" | "surface">): boolean;
|
|
74
|
+
feature(feature: string, event?: Omit<ProductEvent, "name" | "feature">): boolean;
|
|
75
|
+
flush: () => Promise<boolean>;
|
|
76
|
+
shutdown: () => Promise<boolean>;
|
|
77
|
+
};
|
package/dist/product.js
CHANGED
|
@@ -26,16 +26,30 @@ export function createProductAnalytics(options) {
|
|
|
26
26
|
track(event) {
|
|
27
27
|
if (stopped || queue.length >= MAX_QUEUE || !NAME.test(event.name))
|
|
28
28
|
return false;
|
|
29
|
-
const active = trace.getSpan(context.active())?.spanContext();
|
|
30
29
|
const ctx = { ...bound, ...event };
|
|
30
|
+
if (options.eventDefinitions && (!Object.hasOwn(options.eventDefinitions, event.name) ||
|
|
31
|
+
options.eventDefinitions[event.name]?.required?.some(key => !text(ctx[key]))))
|
|
32
|
+
return false;
|
|
33
|
+
const active = trace.getSpan(context.active())?.spanContext();
|
|
31
34
|
const attrs = {
|
|
32
|
-
"event.name": event.name, "event.domain": "product", "event.id": crypto.randomUUID(),
|
|
35
|
+
"event.name": event.name, "event.domain": "product", "event.id": text(event.eventId) || crypto.randomUUID(), "heystack.schema.version": "1",
|
|
33
36
|
};
|
|
34
37
|
for (const [key, v] of Object.entries({ "enduser.id": ctx.userId, "session.id": ctx.sessionId,
|
|
35
|
-
"heystack.feature": ctx.feature, "heystack.surface": ctx.surface, "heystack.action": event.action, "heystack.outcome": event.outcome })) {
|
|
38
|
+
"heystack.attempt.id": event.attemptId, "heystack.feature": ctx.feature, "heystack.surface": ctx.surface, "heystack.action": event.action, "heystack.outcome": event.outcome })) {
|
|
36
39
|
if (text(v))
|
|
37
40
|
attrs[key] = text(v);
|
|
38
41
|
}
|
|
42
|
+
if (options.allowUserProfiles && text(ctx.userId) && ctx.userProfile) {
|
|
43
|
+
attrs["heystack.user.display_name"] = text(ctx.userProfile.displayName);
|
|
44
|
+
attrs["heystack.user.source"] = ctx.userProfile.source === "clerk" ? "clerk" : "app";
|
|
45
|
+
}
|
|
46
|
+
if (options.allowNetworkContext && ctx.network) {
|
|
47
|
+
for (const [key, v] of Object.entries({ "client.address": ctx.network.ip, "geo.country.code": ctx.network.country,
|
|
48
|
+
"geo.locality.region": ctx.network.region, "geo.locality.name": ctx.network.city, "heystack.geo.source": ctx.network.source })) {
|
|
49
|
+
if (text(v))
|
|
50
|
+
attrs[key] = text(v);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
39
53
|
for (const key of allowed) {
|
|
40
54
|
const v = event.properties?.[key];
|
|
41
55
|
if (typeof v === "string")
|
|
@@ -95,3 +109,27 @@ export function createProductAnalytics(options) {
|
|
|
95
109
|
return { ...tracker({}), flush, async shutdown() { stopped = true; if (timer)
|
|
96
110
|
clearInterval(timer); return flush(); } };
|
|
97
111
|
}
|
|
112
|
+
/** Per-browser-session facade. Do not share this mutable identity facade on servers. */
|
|
113
|
+
export function createBrowserProductAnalytics(options) {
|
|
114
|
+
const client = createProductAnalytics(options);
|
|
115
|
+
let sessionId = options.sessionId ?? crypto.randomUUID();
|
|
116
|
+
let user = {};
|
|
117
|
+
const bound = () => client.withContext({ ...user, sessionId });
|
|
118
|
+
return {
|
|
119
|
+
get sessionId() { return sessionId; },
|
|
120
|
+
/** Starts a fresh session on identity changes. Reconfigure replay with returned ID. */
|
|
121
|
+
identify(userId, profile) {
|
|
122
|
+
if (user.userId !== userId)
|
|
123
|
+
sessionId = crypto.randomUUID();
|
|
124
|
+
user = { userId, userProfile: profile };
|
|
125
|
+
return sessionId;
|
|
126
|
+
},
|
|
127
|
+
/** Logout boundary: clear identity and rotate session. Reconfigure replay too. */
|
|
128
|
+
reset() { user = {}; sessionId = crypto.randomUUID(); return sessionId; },
|
|
129
|
+
track(event) { return bound().track({ ...event, userId: user.userId, userProfile: user.userProfile, sessionId }); },
|
|
130
|
+
screen(surface, event = {}) { return this.track({ ...event, name: "screen.viewed", surface }); },
|
|
131
|
+
feature(feature, event = {}) { return this.track({ ...event, name: "feature.used", feature }); },
|
|
132
|
+
flush: client.flush,
|
|
133
|
+
shutdown: client.shutdown,
|
|
134
|
+
};
|
|
135
|
+
}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Replacement written in place of a redacted query/fragment parameter value. */
|
|
2
|
+
export declare const REDACTED = "REDACTED";
|
|
3
|
+
/**
|
|
4
|
+
* Query/fragment parameter names whose VALUES are replaced with `REDACTED`
|
|
5
|
+
* before a URL is recorded. Compared case-insensitively against the decoded key.
|
|
6
|
+
* `state` is intentionally NOT listed (OAuth `state` is an anti-CSRF nonce, not a
|
|
7
|
+
* credential, and it is useful when debugging a broken login redirect).
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_REDACTED_QUERY_PARAMS: readonly string[];
|
|
10
|
+
/** Options controlling how URLs are redacted before they are recorded. */
|
|
11
|
+
export interface UrlRedactionOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Extra query/fragment parameter names to redact, ADDED to
|
|
14
|
+
* {@link DEFAULT_REDACTED_QUERY_PARAMS} (case-insensitive). Pass `false` to
|
|
15
|
+
* turn query/fragment parameter redaction off entirely. Credentials embedded
|
|
16
|
+
* as URL userinfo (`https://user:pass@host`) are always stripped.
|
|
17
|
+
*/
|
|
18
|
+
redactQueryParams?: readonly string[] | false;
|
|
19
|
+
}
|
|
20
|
+
/** Privacy options shared by the server entries (`/node`, `/next`, `/workers`). */
|
|
21
|
+
export interface SpanPrivacyOptions extends UrlRedactionOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Record the end-user's IP address (`client.address`, plus `http.client_ip` /
|
|
24
|
+
* peer-address attributes on SERVER spans under `/node`). **Off by default** —
|
|
25
|
+
* an IP address is personal data in most jurisdictions. Set `true` only if you
|
|
26
|
+
* have a lawful basis and have told your users.
|
|
27
|
+
*/
|
|
28
|
+
captureClientIp?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export type UrlRedactor = (url: string) => string;
|
|
31
|
+
/** Build a reusable redactor (pre-computes the key set). */
|
|
32
|
+
export declare function makeUrlRedactor(opts?: UrlRedactionOptions): UrlRedactor;
|
|
33
|
+
/**
|
|
34
|
+
* Redact secrets from a URL before it is recorded: strips userinfo
|
|
35
|
+
* (`https://user:pass@host` → `https://host`) and replaces the VALUE of every
|
|
36
|
+
* sensitive query or fragment parameter (see
|
|
37
|
+
* {@link DEFAULT_REDACTED_QUERY_PARAMS}) with `REDACTED`. Everything else —
|
|
38
|
+
* scheme, host, path, non-sensitive parameters, plain `#anchors` — is kept
|
|
39
|
+
* byte-for-byte. Works on absolute and relative URLs (`/cb?code=…`).
|
|
40
|
+
*/
|
|
41
|
+
export declare function redactUrl(url: string, opts?: UrlRedactionOptions): string;
|
|
42
|
+
/** Span/log attributes that hold a full or relative URL. */
|
|
43
|
+
export declare const URL_ATTRS: readonly ["url.full", "http.url", "http.target", "url.original"];
|
|
44
|
+
/** End-user IP attributes, dropped on every span unless `captureClientIp`. */
|
|
45
|
+
export declare const CLIENT_IP_ATTRS: readonly ["client.address", "http.client_ip"];
|
|
46
|
+
/** Peer-address attributes that equal the end-user (or edge proxy) IP on a SERVER span. */
|
|
47
|
+
export declare const SERVER_PEER_IP_ATTRS: readonly ["net.peer.ip", "network.peer.address", "net.sock.peer.addr"];
|
|
48
|
+
/**
|
|
49
|
+
* Return `attrs` with every URL-bearing attribute redacted and, unless
|
|
50
|
+
* `captureClientIp`, end-user IP attributes removed. Returns the SAME object
|
|
51
|
+
* when nothing changed (cheap common path), otherwise a shallow copy.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applySpanPrivacy(attrs: Record<string, unknown>, o: {
|
|
54
|
+
redact: UrlRedactor;
|
|
55
|
+
captureClientIp?: boolean;
|
|
56
|
+
isServer?: boolean;
|
|
57
|
+
}): Record<string, unknown>;
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// URL + client-IP privacy helpers (shared by /node, /workers and /web)
|
|
3
|
+
//
|
|
4
|
+
// Pure string logic with NO imports — safe in every entry, including the
|
|
5
|
+
// WinterCG-only `/workers` build and the browser `/web` bundle. Deliberately
|
|
6
|
+
// avoids `new URL(...).href` re-serialisation so a redacted URL keeps the exact
|
|
7
|
+
// spelling of everything that was not redacted (and so relative targets such as
|
|
8
|
+
// `http.target` = `/cb?code=…` work without a base).
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/** Replacement written in place of a redacted query/fragment parameter value. */
|
|
11
|
+
export const REDACTED = "REDACTED";
|
|
12
|
+
/**
|
|
13
|
+
* Query/fragment parameter names whose VALUES are replaced with `REDACTED`
|
|
14
|
+
* before a URL is recorded. Compared case-insensitively against the decoded key.
|
|
15
|
+
* `state` is intentionally NOT listed (OAuth `state` is an anti-CSRF nonce, not a
|
|
16
|
+
* credential, and it is useful when debugging a broken login redirect).
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_REDACTED_QUERY_PARAMS = [
|
|
19
|
+
"key",
|
|
20
|
+
"api_key",
|
|
21
|
+
"apikey",
|
|
22
|
+
"api-key",
|
|
23
|
+
"token",
|
|
24
|
+
"access_token",
|
|
25
|
+
"refresh_token",
|
|
26
|
+
"id_token",
|
|
27
|
+
"auth_token",
|
|
28
|
+
"api_token",
|
|
29
|
+
"oauth_token",
|
|
30
|
+
"oauth_signature",
|
|
31
|
+
"code",
|
|
32
|
+
"sig",
|
|
33
|
+
"signature",
|
|
34
|
+
"password",
|
|
35
|
+
"passwd",
|
|
36
|
+
"pass",
|
|
37
|
+
"secret",
|
|
38
|
+
"client_secret",
|
|
39
|
+
"auth",
|
|
40
|
+
"authorization",
|
|
41
|
+
"session",
|
|
42
|
+
"sessionid",
|
|
43
|
+
"session_id",
|
|
44
|
+
"jwt",
|
|
45
|
+
"x-amz-signature",
|
|
46
|
+
"x-amz-credential",
|
|
47
|
+
"x-amz-security-token",
|
|
48
|
+
"x-goog-signature",
|
|
49
|
+
"x-goog-credential",
|
|
50
|
+
];
|
|
51
|
+
function safeDecode(s) {
|
|
52
|
+
try {
|
|
53
|
+
return decodeURIComponent(s.replace(/\+/g, " "));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return s;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Redact sensitive parameter values in a bare `a=1&b=2` string. */
|
|
60
|
+
function redactParams(qs, keys) {
|
|
61
|
+
if (!qs || keys.size === 0)
|
|
62
|
+
return qs;
|
|
63
|
+
let changed = false;
|
|
64
|
+
const parts = qs.split("&").map((part) => {
|
|
65
|
+
const eq = part.indexOf("=");
|
|
66
|
+
if (eq < 0)
|
|
67
|
+
return part; // bare flag, no value to leak
|
|
68
|
+
const key = part.slice(0, eq);
|
|
69
|
+
if (!keys.has(safeDecode(key).trim().toLowerCase()))
|
|
70
|
+
return part;
|
|
71
|
+
if (part.slice(eq + 1) === REDACTED)
|
|
72
|
+
return part;
|
|
73
|
+
changed = true;
|
|
74
|
+
return `${key}=${REDACTED}`;
|
|
75
|
+
});
|
|
76
|
+
return changed ? parts.join("&") : qs;
|
|
77
|
+
}
|
|
78
|
+
/** Strip `user[:password]@` from an absolute (or protocol-relative) URL. */
|
|
79
|
+
const USERINFO_RE = /^((?:[a-zA-Z][a-zA-Z0-9+.-]*:)?\/\/)[^/?#]*@/;
|
|
80
|
+
function redactWith(url, keys) {
|
|
81
|
+
let out = url.replace(USERINFO_RE, "$1");
|
|
82
|
+
const hash = out.indexOf("#");
|
|
83
|
+
let fragment;
|
|
84
|
+
if (hash >= 0) {
|
|
85
|
+
fragment = out.slice(hash + 1);
|
|
86
|
+
out = out.slice(0, hash);
|
|
87
|
+
}
|
|
88
|
+
const q = out.indexOf("?");
|
|
89
|
+
if (q >= 0)
|
|
90
|
+
out = `${out.slice(0, q)}?${redactParams(out.slice(q + 1), keys)}`;
|
|
91
|
+
if (fragment !== undefined) {
|
|
92
|
+
// OAuth implicit flow puts tokens in the fragment (`#access_token=…`); hash
|
|
93
|
+
// routers put a query after a route (`#/cb?code=…`). Plain anchors pass.
|
|
94
|
+
const fq = fragment.indexOf("?");
|
|
95
|
+
if (fq >= 0) {
|
|
96
|
+
fragment = `${fragment.slice(0, fq)}?${redactParams(fragment.slice(fq + 1), keys)}`;
|
|
97
|
+
}
|
|
98
|
+
else if (fragment.includes("=")) {
|
|
99
|
+
fragment = redactParams(fragment, keys);
|
|
100
|
+
}
|
|
101
|
+
out = `${out}#${fragment}`;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function keySet(opts) {
|
|
106
|
+
if (opts?.redactQueryParams === false)
|
|
107
|
+
return new Set();
|
|
108
|
+
const extra = Array.isArray(opts?.redactQueryParams) ? opts.redactQueryParams : [];
|
|
109
|
+
return new Set([...DEFAULT_REDACTED_QUERY_PARAMS, ...extra].map((k) => String(k).trim().toLowerCase()));
|
|
110
|
+
}
|
|
111
|
+
/** Build a reusable redactor (pre-computes the key set). */
|
|
112
|
+
export function makeUrlRedactor(opts) {
|
|
113
|
+
const keys = keySet(opts);
|
|
114
|
+
return (url) => (typeof url === "string" && url ? redactWith(url, keys) : url);
|
|
115
|
+
}
|
|
116
|
+
const defaultRedactor = makeUrlRedactor();
|
|
117
|
+
/**
|
|
118
|
+
* Redact secrets from a URL before it is recorded: strips userinfo
|
|
119
|
+
* (`https://user:pass@host` → `https://host`) and replaces the VALUE of every
|
|
120
|
+
* sensitive query or fragment parameter (see
|
|
121
|
+
* {@link DEFAULT_REDACTED_QUERY_PARAMS}) with `REDACTED`. Everything else —
|
|
122
|
+
* scheme, host, path, non-sensitive parameters, plain `#anchors` — is kept
|
|
123
|
+
* byte-for-byte. Works on absolute and relative URLs (`/cb?code=…`).
|
|
124
|
+
*/
|
|
125
|
+
export function redactUrl(url, opts) {
|
|
126
|
+
return opts ? makeUrlRedactor(opts)(url) : defaultRedactor(url);
|
|
127
|
+
}
|
|
128
|
+
/** Span/log attributes that hold a full or relative URL. */
|
|
129
|
+
export const URL_ATTRS = ["url.full", "http.url", "http.target", "url.original"];
|
|
130
|
+
/** Attributes that hold a bare query string (no leading `?`). */
|
|
131
|
+
const QUERY_ATTRS = ["url.query"];
|
|
132
|
+
/** Attributes that hold a bare fragment (no leading `#`). */
|
|
133
|
+
const FRAGMENT_ATTRS = ["url.fragment"];
|
|
134
|
+
/** End-user IP attributes, dropped on every span unless `captureClientIp`. */
|
|
135
|
+
export const CLIENT_IP_ATTRS = ["client.address", "http.client_ip"];
|
|
136
|
+
/** Peer-address attributes that equal the end-user (or edge proxy) IP on a SERVER span. */
|
|
137
|
+
export const SERVER_PEER_IP_ATTRS = [
|
|
138
|
+
"net.peer.ip",
|
|
139
|
+
"network.peer.address",
|
|
140
|
+
"net.sock.peer.addr",
|
|
141
|
+
];
|
|
142
|
+
/**
|
|
143
|
+
* Return `attrs` with every URL-bearing attribute redacted and, unless
|
|
144
|
+
* `captureClientIp`, end-user IP attributes removed. Returns the SAME object
|
|
145
|
+
* when nothing changed (cheap common path), otherwise a shallow copy.
|
|
146
|
+
*/
|
|
147
|
+
export function applySpanPrivacy(attrs, o) {
|
|
148
|
+
let out;
|
|
149
|
+
const set = (k, v) => {
|
|
150
|
+
out ??= { ...attrs };
|
|
151
|
+
if (v === undefined)
|
|
152
|
+
delete out[k];
|
|
153
|
+
else
|
|
154
|
+
out[k] = v;
|
|
155
|
+
};
|
|
156
|
+
for (const k of URL_ATTRS) {
|
|
157
|
+
const v = attrs[k];
|
|
158
|
+
if (typeof v === "string") {
|
|
159
|
+
const r = o.redact(v);
|
|
160
|
+
if (r !== v)
|
|
161
|
+
set(k, r);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const k of QUERY_ATTRS) {
|
|
165
|
+
const v = attrs[k];
|
|
166
|
+
if (typeof v === "string" && v) {
|
|
167
|
+
const r = o.redact(`?${v.replace(/^\?/, "")}`).slice(1);
|
|
168
|
+
if (r !== v)
|
|
169
|
+
set(k, r);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const k of FRAGMENT_ATTRS) {
|
|
173
|
+
const v = attrs[k];
|
|
174
|
+
if (typeof v === "string" && v) {
|
|
175
|
+
const r = o.redact(`#${v.replace(/^#/, "")}`).slice(1);
|
|
176
|
+
if (r !== v)
|
|
177
|
+
set(k, r);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!o.captureClientIp) {
|
|
181
|
+
for (const k of CLIENT_IP_ATTRS)
|
|
182
|
+
if (k in attrs)
|
|
183
|
+
set(k, undefined);
|
|
184
|
+
if (o.isServer)
|
|
185
|
+
for (const k of SERVER_PEER_IP_ATTRS)
|
|
186
|
+
if (k in attrs)
|
|
187
|
+
set(k, undefined);
|
|
188
|
+
}
|
|
189
|
+
return out ?? attrs;
|
|
190
|
+
}
|
package/dist/web.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type UrlRedactor } from "./redact.js";
|
|
1
2
|
/** The replay chunk metadata sent alongside the events array. Defined locally so
|
|
2
3
|
* the published SDK does not depend on the private @heystack/schema package. */
|
|
3
4
|
export interface ReplayChunkMeta {
|
|
@@ -130,6 +131,14 @@ export interface InstrumentWebOptions {
|
|
|
130
131
|
* Rate-capped and recursion-guarded so it can never feed a self-export loop.
|
|
131
132
|
*/
|
|
132
133
|
captureConsole?: "error" | "warn" | false;
|
|
134
|
+
/**
|
|
135
|
+
* Extra query/fragment parameter names whose values are replaced with
|
|
136
|
+
* `REDACTED` in every URL this SDK records (fetch spans, page URL on errors,
|
|
137
|
+
* console logs, bug reports and replay). Added to the built-in list (`token`,
|
|
138
|
+
* `code`, `key`, `apikey`, `access_token`, `signature`, …); userinfo is always
|
|
139
|
+
* stripped. Pass `false` to turn parameter redaction off.
|
|
140
|
+
*/
|
|
141
|
+
redactQueryParams?: readonly string[] | false;
|
|
133
142
|
}
|
|
134
143
|
/**
|
|
135
144
|
* Pure: is browser distributed tracing active for this run? Requires BOTH the app
|
|
@@ -146,6 +155,33 @@ export declare function tracingActive(opts: Pick<InstrumentWebOptions, "tracing"
|
|
|
146
155
|
* (`cfg.errors_enabled`). **Fail-open**: an absent/`true` flag counts as enabled.
|
|
147
156
|
*/
|
|
148
157
|
export declare function errorCaptureActive(opts: Pick<InstrumentWebOptions, "errors">, cfg: Pick<RecorderConfig, "errors_enabled">): boolean;
|
|
158
|
+
/** rrweb `record()` privacy options for a masking mode. */
|
|
159
|
+
export interface ReplayPrivacyOptions {
|
|
160
|
+
maskAllInputs: boolean;
|
|
161
|
+
maskTextSelector: string;
|
|
162
|
+
blockSelector: string;
|
|
163
|
+
maskInputFn: (text: string, element: HTMLElement) => string;
|
|
164
|
+
maskTextFn: (text: string, element: HTMLElement | null) => string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Pure: the rrweb privacy options for a masking mode. Anything other than an
|
|
168
|
+
* explicit `"relaxed"` is treated as `"strict"` (fail-closed).
|
|
169
|
+
*
|
|
170
|
+
* - **strict** (default): ALL text and ALL input values are masked
|
|
171
|
+
* (`maskTextSelector: "*"` + `maskAllInputs`). Mark an element with
|
|
172
|
+
* `data-hs-unmask` to record it (and its descendants) in cleartext; a nested
|
|
173
|
+
* `data-hs-mask` re-masks. Password inputs are ALWAYS masked, even inside a
|
|
174
|
+
* `data-hs-unmask` region.
|
|
175
|
+
* - **relaxed**: text is recorded except inside `data-hs-mask`; input values are
|
|
176
|
+
* recorded except passwords (rrweb's default).
|
|
177
|
+
*
|
|
178
|
+
* Both modes drop `data-hs-block` elements entirely (recorded as a placeholder
|
|
179
|
+
* box). Masking applies to text nodes and input values only — element ATTRIBUTES
|
|
180
|
+
* (`title`, `alt`, `placeholder`, `aria-label`, link `href`s) are not masked by
|
|
181
|
+
* rrweb, so use `data-hs-block` for regions whose attributes are sensitive.
|
|
182
|
+
*/
|
|
183
|
+
export declare function replayPrivacyOptions(mode: RecorderConfig["masking_mode"] | undefined): ReplayPrivacyOptions;
|
|
184
|
+
export declare function redactReplayEvent(event: unknown, redact?: UrlRedactor): unknown;
|
|
149
185
|
/** A caller-owned session UUID allows product activity to correlate with replay. */
|
|
150
186
|
export declare function resolveWebSessionId(candidate?: string): string;
|
|
151
187
|
/** Entry point: fetch config, decide sampling, start rrweb, stream chunks.
|
|
@@ -236,6 +272,8 @@ export interface TracingPatchOpts {
|
|
|
236
272
|
/** Cross-origin backends allowed to receive trace headers (same-origin always is).
|
|
237
273
|
* Third-party / non-listed cross-origin calls are passed through untouched. */
|
|
238
274
|
propagateTargets?: ReadonlyArray<string | RegExp>;
|
|
275
|
+
/** URL redactor for the recorded `url.full` (default: built-in redaction). */
|
|
276
|
+
redactUrl?: UrlRedactor;
|
|
239
277
|
rng?: () => number;
|
|
240
278
|
}
|
|
241
279
|
/** Patch window.fetch to emit a real CLIENT span per outbound call, inject that
|
|
@@ -322,6 +360,8 @@ export interface ErrorCaptureOpts {
|
|
|
322
360
|
rateCap?: number;
|
|
323
361
|
/** Rate window in ms (default 60_000). */
|
|
324
362
|
rateWindowMs?: number;
|
|
363
|
+
/** URL redactor for the recorded page URL (default: built-in redaction). */
|
|
364
|
+
redactUrl?: UrlRedactor;
|
|
325
365
|
}
|
|
326
366
|
/** Wire up `window.onerror` / `unhandledrejection` (+ optional console) capture.
|
|
327
367
|
* Returns an unpatch function. Recursion-guarded (anything logged on our own
|
|
@@ -356,6 +396,8 @@ export interface BugReportSession {
|
|
|
356
396
|
logsExporter: BrowserLogsExporter;
|
|
357
397
|
version?: string;
|
|
358
398
|
build?: string;
|
|
399
|
+
/** URL redactor for the attached page URL (default: built-in redaction). */
|
|
400
|
+
redactUrl?: UrlRedactor;
|
|
359
401
|
}
|
|
360
402
|
/** Internal: instrumentWeb() registers the active session here (and clears it with
|
|
361
403
|
* `null` on stop) so the module-level reportBug() works after instrumentWeb() is
|