ytracking-web 0.1.1 → 0.1.2
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 +4 -3
- package/package.json +1 -1
- package/src/client.ts +114 -0
- package/src/index.ts +1 -0
- package/src/transport.ts +3 -1
package/README.md
CHANGED
|
@@ -21,9 +21,9 @@ npm install ytracking-web
|
|
|
21
21
|
|
|
22
22
|
## 發佈至 npm
|
|
23
23
|
|
|
24
|
-
1.
|
|
24
|
+
1. 於倉庫根目錄先跑契約檢查:`npm run check:web-sdk`、`npm run check:openapi-drift`、(建議)`npm run build:web-sdk`。(本機 **`npm run publish:ytracking-web`** 已內含 `check:web-sdk` 與 `check:openapi-drift`。)
|
|
25
25
|
2. **GitHub Actions**:「Publish ytracking-web to npm」workflow(`workflow_dispatch`)。需在 repo 設定 **`NPM_TOKEN`**(npm automation access token,具 publish 權限)。
|
|
26
|
-
3. **本機(免 `npm login`,適合首次發佈作法 B)**:在倉庫根目錄設定 **`NPM_TOKEN`** 後執行 **`npm run publish:ytracking-web`**(會先跑 `check:web-sdk` 再 `npm publish
|
|
26
|
+
3. **本機(免 `npm login`,適合首次發佈作法 B)**:在倉庫根目錄設定 **`NPM_TOKEN`** 後執行 **`npm run publish:ytracking-web`**(會先跑 `check:web-sdk` 再 `npm publish`)。若有 API 契約改動,請先在 root 執行 `npm run check:openapi-drift` 並同步更新 `docs/API.md` / `docs/openapi.yaml`。
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
## 使用(IIFE)
|
|
@@ -55,6 +55,7 @@ npm install ytracking-web
|
|
|
55
55
|
- `track(type, { props })` — 對應 `POST /v1/collect` envelope
|
|
56
56
|
- `trackPageView()` — `page_view`
|
|
57
57
|
- `install({ clickId? })` — 內嵌 **`POST /v1/install`**(無 `appId` 之 embed 形態)
|
|
58
|
+
- `issueAttributionToken({ clickId?, ttlSeconds?, campaignId?, appId?, deferredContext? })` — 同步 **`POST /v1/attribution/token`**(不入佇列);沿用 `setContext` 之 visitor/session/click 等
|
|
58
59
|
- `flush()` — 手動送出佇列
|
|
59
60
|
- `setContext({ visitorId, sessionId, clickId, campaignId, appId })` — 後續事件帶入
|
|
60
61
|
- `shutdown()` — 停止定時 flush
|
|
@@ -64,7 +65,7 @@ npm install ytracking-web
|
|
|
64
65
|
- 每筆事件在 `event.props` 附 **`_ytSdkEventId`**(支援 `crypto.randomUUID` 時為 UUID)與 **`_ytSdk: "web-v1"`**;同時在 JSON 根層帶 **`idempotencyKey`**(與該 UUID 相同),與 **`POST /v1/collect`** 去重契約對齊(見 [API.md](../../docs/API.md) §2.2)。重試/重送同一佇列項目時沿用同一 id,consumer 只會落庫一次。
|
|
65
66
|
- `collect` 出站 `Idempotency-Key` 會優先使用 payload 根層 `idempotencyKey`(若缺失才回退 queue id),確保 header 與 body 同鍵。
|
|
66
67
|
- collect 成功回應若包含 `visitorId`,SDK 會寫入 localStorage(`yt_vid_v1`)並自動帶入後續事件 context;可在 cookie 受限環境維持 visitor 關聯。
|
|
67
|
-
- 須符合 `sites.allowed_origins`(見 [SITE_AND_EMBED.md](../../docs/SITE_AND_EMBED.md))。
|
|
68
|
+
- 須符合 `sites.allowed_origins`(見 [SITE_AND_EMBED.md](../../docs/SITE_AND_EMBED.md))。SDK collect 出站採 `credentials: "omit"`,不依賴瀏覽器 cookie。
|
|
68
69
|
- fallback 會維護 endpoint pool + cursor;成功入口可刷新新 hosts,不可達時會送 best-effort `domain-health/report`(含節流)。
|
|
69
70
|
- `Retry-After` 同時支援秒數與 HTTP-date;`shutdown()` 會解除 `visibilitychange` listener,避免重複初始化時累積監聽器。
|
|
70
71
|
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -45,6 +45,10 @@ function installUrl(origin: string): string {
|
|
|
45
45
|
return `${origin}/v1/install`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function attributionTokenUrl(origin: string): string {
|
|
49
|
+
return `${origin}/v1/attribution/token`;
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
export class YTrackingWebClient {
|
|
49
53
|
private cfg: Required<
|
|
50
54
|
Pick<InitConfig, "siteId" | "baseUrls"> & {
|
|
@@ -200,6 +204,105 @@ export class YTrackingWebClient {
|
|
|
200
204
|
});
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Issue a one-time deferred deep link token (`POST /v1/attribution/token`).
|
|
209
|
+
* Synchronous HTTP (not queued); rotates ingestion origins on success like collect/install.
|
|
210
|
+
*/
|
|
211
|
+
async issueAttributionToken(opts?: {
|
|
212
|
+
clickId?: string;
|
|
213
|
+
ttlSeconds?: number;
|
|
214
|
+
campaignId?: string;
|
|
215
|
+
appId?: string;
|
|
216
|
+
deferredContext?: Record<string, unknown>;
|
|
217
|
+
}): Promise<{ attributionToken: string; expiresAt: string }> {
|
|
218
|
+
const body: Record<string, unknown> = { siteId: this.cfg.siteId };
|
|
219
|
+
const cid = opts?.clickId?.trim() || this.context.clickId?.trim();
|
|
220
|
+
if (cid) body.clickId = cid;
|
|
221
|
+
const vid = this.context.visitorId?.trim();
|
|
222
|
+
if (vid) body.visitorId = vid;
|
|
223
|
+
const sid = this.context.sessionId?.trim();
|
|
224
|
+
if (sid) body.sessionId = sid;
|
|
225
|
+
const camp = opts?.campaignId?.trim() || this.context.campaignId?.trim();
|
|
226
|
+
if (camp) body.campaignId = camp;
|
|
227
|
+
const aid = opts?.appId?.trim() || this.context.appId?.trim();
|
|
228
|
+
if (aid) body.appId = aid;
|
|
229
|
+
if (opts?.ttlSeconds != null) body.ttlSeconds = opts.ttlSeconds;
|
|
230
|
+
if (
|
|
231
|
+
opts?.deferredContext &&
|
|
232
|
+
typeof opts.deferredContext === "object" &&
|
|
233
|
+
!Array.isArray(opts.deferredContext)
|
|
234
|
+
) {
|
|
235
|
+
body.deferredContext = opts.deferredContext;
|
|
236
|
+
}
|
|
237
|
+
const payload = JSON.stringify(body);
|
|
238
|
+
let lastRetryAfter: number | undefined;
|
|
239
|
+
|
|
240
|
+
for (const origin of this.originsInOrder()) {
|
|
241
|
+
const url = attributionTokenUrl(origin);
|
|
242
|
+
const res = await postJson(
|
|
243
|
+
url,
|
|
244
|
+
payload,
|
|
245
|
+
this.cfg.requestTimeoutMs,
|
|
246
|
+
undefined,
|
|
247
|
+
);
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
log(
|
|
250
|
+
this.cfg.debug,
|
|
251
|
+
"issueAttributionToken transport fail",
|
|
252
|
+
origin,
|
|
253
|
+
res.kind,
|
|
254
|
+
);
|
|
255
|
+
if (res.retryAfterMs !== undefined) lastRetryAfter = res.retryAfterMs;
|
|
256
|
+
if (res.kind === "network" || res.kind === "timeout") {
|
|
257
|
+
await this.reportDomainUnreachable(origin, res.kind);
|
|
258
|
+
this.rotateCursorAfter(origin);
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (res.retryAfterMs !== undefined) lastRetryAfter = res.retryAfterMs;
|
|
263
|
+
|
|
264
|
+
if (res.status === 201) {
|
|
265
|
+
const j = res.bodyJson as {
|
|
266
|
+
attributionToken?: unknown;
|
|
267
|
+
expiresAt?: unknown;
|
|
268
|
+
};
|
|
269
|
+
const tok =
|
|
270
|
+
typeof j?.attributionToken === "string"
|
|
271
|
+
? j.attributionToken.trim()
|
|
272
|
+
: "";
|
|
273
|
+
const exp = typeof j?.expiresAt === "string" ? j.expiresAt.trim() : "";
|
|
274
|
+
if (tok && exp) {
|
|
275
|
+
this.setCursorToOrigin(origin);
|
|
276
|
+
await this.refreshEndpointPool(origin);
|
|
277
|
+
return { attributionToken: tok, expiresAt: exp };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (isNonRetryableStatus(res.status)) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`YTrackingWeb: issueAttributionToken failed with HTTP ${res.status}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (isRetryableStatus(res.status)) {
|
|
286
|
+
log(
|
|
287
|
+
this.cfg.debug,
|
|
288
|
+
"issueAttributionToken retryable",
|
|
289
|
+
res.status,
|
|
290
|
+
origin,
|
|
291
|
+
);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
throw new Error(
|
|
295
|
+
`YTrackingWeb: issueAttributionToken failed with HTTP ${res.status}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
throw new Error(
|
|
300
|
+
lastRetryAfter !== undefined
|
|
301
|
+
? `YTrackingWeb: issueAttributionToken failed (Retry-After hint ${lastRetryAfter}ms)`
|
|
302
|
+
: "YTrackingWeb: issueAttributionToken failed on all origins",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
203
306
|
private async enqueue(p: {
|
|
204
307
|
kind: OutboxRecord["kind"];
|
|
205
308
|
payload: string;
|
|
@@ -567,6 +670,17 @@ export async function install(opts?: { clickId?: string }): Promise<void> {
|
|
|
567
670
|
return singleton.install(opts);
|
|
568
671
|
}
|
|
569
672
|
|
|
673
|
+
export async function issueAttributionToken(opts?: {
|
|
674
|
+
clickId?: string;
|
|
675
|
+
ttlSeconds?: number;
|
|
676
|
+
campaignId?: string;
|
|
677
|
+
appId?: string;
|
|
678
|
+
deferredContext?: Record<string, unknown>;
|
|
679
|
+
}): Promise<{ attributionToken: string; expiresAt: string }> {
|
|
680
|
+
if (!singleton) throw new Error("YTrackingWeb: call init() first");
|
|
681
|
+
return singleton.issueAttributionToken(opts);
|
|
682
|
+
}
|
|
683
|
+
|
|
570
684
|
export function setContext(partial: {
|
|
571
685
|
visitorId?: string;
|
|
572
686
|
sessionId?: string;
|
package/src/index.ts
CHANGED
package/src/transport.ts
CHANGED
|
@@ -34,7 +34,9 @@ export async function postJson(
|
|
|
34
34
|
method: "POST",
|
|
35
35
|
headers: hdrs,
|
|
36
36
|
body,
|
|
37
|
-
|
|
37
|
+
// Collect does not depend on cookies. Keep this non-credentialed so
|
|
38
|
+
// wildcard ACAO ("*") deployments remain browser-compatible.
|
|
39
|
+
credentials: "omit",
|
|
38
40
|
signal: ctrl.signal,
|
|
39
41
|
keepalive: true,
|
|
40
42
|
});
|