@yuanze_dev/tracker 0.3.1 → 0.5.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/autocapture.js +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/redact.d.ts +2 -0
- package/dist/redact.js +51 -0
- package/dist/tracker.d.ts +5 -0
- package/dist/tracker.js +33 -6
- package/package.json +1 -1
package/dist/autocapture.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactUrl } from "./redact.js";
|
|
1
2
|
const DEFAULT_ALLOW = 'a,button,[role="button"],[role="link"],[role="tab"],input[type="submit"],input[type="button"],summary';
|
|
2
3
|
const DEFAULT_DENY = '[data-no-track],input[type="password"],[type="password"],.yz-no-track';
|
|
3
4
|
const SENSITIVE_TEXT = /(\d[\d\s-]{9,}\d)|([\w.+-]+@[\w-]+\.[\w.]+)|(\d{15,19})/g;
|
|
@@ -38,9 +39,9 @@ export function setupAutocapture(tracker, options = {}) {
|
|
|
38
39
|
return;
|
|
39
40
|
const { captureClicks = true, allow = DEFAULT_ALLOW, deny = DEFAULT_DENY, sampleRate = 1, maxTextLength = 64, } = options;
|
|
40
41
|
const sendPageview = () => tracker.track("$pageview", {
|
|
41
|
-
url: location.pathname + location.search,
|
|
42
|
+
url: redactUrl(location.pathname + location.search),
|
|
42
43
|
title: document.title,
|
|
43
|
-
referrer: document.referrer,
|
|
44
|
+
referrer: redactUrl(document.referrer),
|
|
44
45
|
});
|
|
45
46
|
sendPageview();
|
|
46
47
|
const patch = (key) => {
|
|
@@ -90,7 +91,7 @@ export function setupAutocapture(tracker, options = {}) {
|
|
|
90
91
|
props.text = text;
|
|
91
92
|
const href = el.getAttribute("href");
|
|
92
93
|
if (href && !href.startsWith("javascript:"))
|
|
93
|
-
props.href = href;
|
|
94
|
+
props.href = redactUrl(href);
|
|
94
95
|
tracker.trackUnsafe("$autocapture", props);
|
|
95
96
|
}, true);
|
|
96
97
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type { TrackerOptions, Sender } from "./tracker.js";
|
|
|
6
6
|
export { setupAutocapture, sanitizeText, elementSelector } from "./autocapture.js";
|
|
7
7
|
export type { AutocaptureOptions } from "./autocapture.js";
|
|
8
8
|
export { collectEnvContext } from "./context.js";
|
|
9
|
+
export { redactUrl, CREDENTIAL_PARAMS } from "./redact.js";
|
|
9
10
|
export type { EnvContext } from "./context.js";
|
|
10
11
|
export { SYSTEM_EVENTS, isSystemEvent } from "./events.js";
|
|
11
12
|
export type { EventName, EventProps, SystemEvents, ProjectEvents } from "./events.js";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { collectEnvContext } from "./context.js";
|
|
|
4
4
|
export { Tracker, defaultFetchSender } from "./tracker.js";
|
|
5
5
|
export { setupAutocapture, sanitizeText, elementSelector } from "./autocapture.js";
|
|
6
6
|
export { collectEnvContext } from "./context.js";
|
|
7
|
+
export { redactUrl, CREDENTIAL_PARAMS } from "./redact.js";
|
|
7
8
|
export { SYSTEM_EVENTS, isSystemEvent } from "./events.js";
|
|
8
9
|
let instance = null;
|
|
9
10
|
export function init(opts) {
|
package/dist/redact.d.ts
ADDED
package/dist/redact.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const CREDENTIAL_PARAMS = [
|
|
2
|
+
"token",
|
|
3
|
+
"access_token",
|
|
4
|
+
"id_token",
|
|
5
|
+
"refresh_token",
|
|
6
|
+
"code",
|
|
7
|
+
];
|
|
8
|
+
const isCredential = (key, params) => params.includes(key.toLowerCase());
|
|
9
|
+
function redactQuery(query, params) {
|
|
10
|
+
if (!query)
|
|
11
|
+
return query;
|
|
12
|
+
let touched = false;
|
|
13
|
+
const kept = [];
|
|
14
|
+
for (const pair of query.split("&")) {
|
|
15
|
+
if (!pair)
|
|
16
|
+
continue;
|
|
17
|
+
const key = pair.split("=")[0];
|
|
18
|
+
let name = key;
|
|
19
|
+
try {
|
|
20
|
+
name = decodeURIComponent(key);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
}
|
|
24
|
+
if (isCredential(name, params)) {
|
|
25
|
+
touched = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
kept.push(pair);
|
|
29
|
+
}
|
|
30
|
+
return touched ? kept.join("&") : query;
|
|
31
|
+
}
|
|
32
|
+
export function redactUrl(input, params = CREDENTIAL_PARAMS) {
|
|
33
|
+
if (!input)
|
|
34
|
+
return input;
|
|
35
|
+
const cut = (s, sep) => {
|
|
36
|
+
const i = s.indexOf(sep);
|
|
37
|
+
return i === -1 ? [s, ""] : [s.slice(0, i), s.slice(i + 1)];
|
|
38
|
+
};
|
|
39
|
+
const [beforeHash, hash] = cut(input, "#");
|
|
40
|
+
const [base, query] = cut(beforeHash, "?");
|
|
41
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(base) && !/^https?:/i.test(base))
|
|
42
|
+
return input;
|
|
43
|
+
const nextQuery = redactQuery(query, params);
|
|
44
|
+
const nextHash = hash.includes("=") ? redactQuery(hash, params) : hash;
|
|
45
|
+
let out = base;
|
|
46
|
+
if (query)
|
|
47
|
+
out += nextQuery ? `?${nextQuery}` : "";
|
|
48
|
+
if (hash)
|
|
49
|
+
out += nextHash ? `#${nextHash}` : "";
|
|
50
|
+
return out;
|
|
51
|
+
}
|
package/dist/tracker.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type TrackerOptions = {
|
|
|
10
10
|
flushMs?: number;
|
|
11
11
|
autocapture?: boolean;
|
|
12
12
|
sender?: Sender;
|
|
13
|
+
maxQueueSize?: number;
|
|
13
14
|
};
|
|
14
15
|
export declare class Tracker {
|
|
15
16
|
private queue;
|
|
@@ -23,6 +24,9 @@ export declare class Tracker {
|
|
|
23
24
|
readonly appVersion?: string;
|
|
24
25
|
readonly maxBatch: number;
|
|
25
26
|
readonly flushMs: number;
|
|
27
|
+
readonly maxQueueSize: number;
|
|
28
|
+
private flushing;
|
|
29
|
+
private retryCount;
|
|
26
30
|
constructor(opts: TrackerOptions);
|
|
27
31
|
private anonId;
|
|
28
32
|
register(props: Record<string, unknown>): void;
|
|
@@ -32,6 +36,7 @@ export declare class Tracker {
|
|
|
32
36
|
track<K extends EventName>(name: K, properties?: EventProps<K>): void;
|
|
33
37
|
trackUnsafe(name: string, properties?: Record<string, unknown>): void;
|
|
34
38
|
private enqueue;
|
|
39
|
+
private trim;
|
|
35
40
|
private schedule;
|
|
36
41
|
flush(unloading?: boolean): Promise<void>;
|
|
37
42
|
}
|
package/dist/tracker.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const MAX_BATCH_SIZE = 50;
|
|
2
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
1
3
|
function uuid() {
|
|
2
4
|
return globalThis.crypto.randomUUID();
|
|
3
5
|
}
|
|
@@ -13,6 +15,9 @@ export class Tracker {
|
|
|
13
15
|
appVersion;
|
|
14
16
|
maxBatch;
|
|
15
17
|
flushMs;
|
|
18
|
+
maxQueueSize;
|
|
19
|
+
flushing = false;
|
|
20
|
+
retryCount = 0;
|
|
16
21
|
constructor(opts) {
|
|
17
22
|
this.endpoint = opts.endpoint;
|
|
18
23
|
this.writeKey = opts.writeKey;
|
|
@@ -20,6 +25,7 @@ export class Tracker {
|
|
|
20
25
|
this.appVersion = opts.appVersion;
|
|
21
26
|
this.maxBatch = opts.maxBatch ?? 5;
|
|
22
27
|
this.flushMs = opts.flushMs ?? 5000;
|
|
28
|
+
this.maxQueueSize = opts.maxQueueSize ?? 500;
|
|
23
29
|
this.send = opts.sender ?? defaultFetchSender;
|
|
24
30
|
this.distinctId = opts.distinctId || this.anonId();
|
|
25
31
|
if (typeof document !== "undefined") {
|
|
@@ -80,34 +86,55 @@ export class Tracker {
|
|
|
80
86
|
clientTime: new Date().toISOString(),
|
|
81
87
|
properties: { ...this.superProps, ...properties },
|
|
82
88
|
});
|
|
89
|
+
this.trim();
|
|
83
90
|
if (this.queue.length >= this.maxBatch)
|
|
84
91
|
this.flush();
|
|
85
92
|
else
|
|
86
93
|
this.schedule();
|
|
87
94
|
}
|
|
88
|
-
|
|
95
|
+
trim() {
|
|
96
|
+
const overflow = this.queue.length - this.maxQueueSize;
|
|
97
|
+
if (overflow > 0) {
|
|
98
|
+
this.queue.splice(0, overflow);
|
|
99
|
+
console.warn(`[tracker] 队列超过 ${this.maxQueueSize} 条,丢弃最旧的 ${overflow} 条`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
schedule(delayMs = this.flushMs) {
|
|
89
103
|
if (this.timer)
|
|
90
104
|
return;
|
|
91
|
-
this.timer = setTimeout(() => this.flush(),
|
|
105
|
+
this.timer = setTimeout(() => this.flush(), delayMs);
|
|
92
106
|
}
|
|
93
107
|
async flush(unloading = false) {
|
|
94
108
|
if (this.timer) {
|
|
95
109
|
clearTimeout(this.timer);
|
|
96
110
|
this.timer = null;
|
|
97
111
|
}
|
|
98
|
-
if (this.queue.length === 0)
|
|
112
|
+
if (this.queue.length === 0 || (this.flushing && !unloading))
|
|
99
113
|
return;
|
|
100
|
-
|
|
114
|
+
this.flushing = true;
|
|
115
|
+
const batch = this.queue.splice(0, Math.min(MAX_BATCH_SIZE, this.queue.length));
|
|
101
116
|
const body = JSON.stringify({ events: batch });
|
|
102
117
|
const headers = { "Content-Type": "application/json", "x-write-key": this.writeKey };
|
|
103
118
|
try {
|
|
104
119
|
await this.send(this.endpoint, body, headers, unloading);
|
|
120
|
+
this.retryCount = 0;
|
|
105
121
|
}
|
|
106
122
|
catch (err) {
|
|
107
|
-
if (err?.retryable !== false)
|
|
123
|
+
if (err?.retryable !== false) {
|
|
108
124
|
this.queue.unshift(...batch);
|
|
109
|
-
|
|
125
|
+
this.trim();
|
|
126
|
+
this.retryCount += 1;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
110
129
|
console.warn("[tracker] 丢弃一批(不可重试):", err?.status);
|
|
130
|
+
this.retryCount = 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
this.flushing = false;
|
|
135
|
+
}
|
|
136
|
+
if (this.queue.length > 0 && !unloading) {
|
|
137
|
+
this.schedule(Math.min(this.flushMs * 2 ** this.retryCount, MAX_BACKOFF_MS));
|
|
111
138
|
}
|
|
112
139
|
}
|
|
113
140
|
}
|