@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.
@@ -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) {
@@ -0,0 +1,2 @@
1
+ export declare const CREDENTIAL_PARAMS: readonly ["token", "access_token", "id_token", "refresh_token", "code"];
2
+ export declare function redactUrl(input: string, params?: readonly string[]): string;
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
- schedule() {
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(), this.flushMs);
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
- const batch = this.queue.splice(0, this.queue.length);
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
- else
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuanze_dev/tracker",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "原则数据埋点 SDK",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",