@faststats/web 0.2.14 → 0.3.1

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.
Files changed (53) hide show
  1. package/dist/chunks/api-urls-nKmJnyjD.js +1 -0
  2. package/dist/chunks/identifiers-DrjjHcKB.js +1 -0
  3. package/dist/chunks/{replay-DvJYurEC.d.ts → replay-DR_T0Iz4.d.ts} +12 -17
  4. package/dist/chunks/send-data-Cb4xlYF0.js +1 -0
  5. package/dist/error.d.ts +4 -12
  6. package/dist/error.js +2 -1
  7. package/dist/feature-flags.d.ts +19 -2
  8. package/dist/feature-flags.js +1 -1
  9. package/dist/index.d.ts +28 -59
  10. package/dist/index.js +1 -1
  11. package/dist/replay.d.ts +2 -2
  12. package/dist/replay.js +1 -1
  13. package/dist/web-vitals.d.ts +5 -8
  14. package/dist/web-vitals.js +1 -1
  15. package/package.json +7 -6
  16. package/CHANGELOG.md +0 -184
  17. package/REPLAY_PAYLOAD.md +0 -64
  18. package/dist/chunks/api-urls-DaeYkG0_.js +0 -1
  19. package/dist/chunks/error-Cd9PTS5v.js +0 -2
  20. package/dist/chunks/feature-flags-BClx56v5.d.ts +0 -19
  21. package/dist/chunks/feature-flags-DSOCIZHK.js +0 -1
  22. package/dist/chunks/replay-rTqcjOo2.js +0 -1
  23. package/dist/chunks/rolldown-runtime-MP-BAFHD.js +0 -1
  24. package/dist/chunks/send-data-B2fYGj6v.js +0 -1
  25. package/dist/chunks/session-manager-Cy63ptPF.js +0 -1
  26. package/dist/chunks/types-CYzR5xtT.js +0 -1
  27. package/dist/chunks/web-vitals-Be-Cg4Po.js +0 -1
  28. package/scripts/check-bundle-size.mjs +0 -96
  29. package/src/analytics.ts +0 -787
  30. package/src/entries/error.ts +0 -6
  31. package/src/entries/feature-flags.ts +0 -5
  32. package/src/entries/main.ts +0 -21
  33. package/src/entries/replay.ts +0 -1
  34. package/src/entries/web-vitals.ts +0 -1
  35. package/src/env.d.ts +0 -2
  36. package/src/error.ts +0 -257
  37. package/src/feature-flags.ts +0 -84
  38. package/src/replay.ts +0 -596
  39. package/src/sdk.ts +0 -8
  40. package/src/utils/api-urls.ts +0 -24
  41. package/src/utils/identifiers.ts +0 -47
  42. package/src/utils/send-data.ts +0 -52
  43. package/src/utils/session-manager.ts +0 -416
  44. package/src/utils/types.ts +0 -15
  45. package/src/web-vitals.ts +0 -159
  46. package/tests/analytics.test.ts +0 -453
  47. package/tests/identifiers.test.ts +0 -78
  48. package/tests/replay.test.ts +0 -582
  49. package/tests/session-manager.test.ts +0 -161
  50. package/tests/web-vitals.test.ts +0 -208
  51. package/tsconfig.json +0 -30
  52. package/tsdown.config.ts +0 -27
  53. package/worker/index.ts +0 -22
@@ -1,582 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { gunzipSync } from "node:zlib";
3
- import ReplayTracker from "../src/replay";
4
- import {
5
- getOrCreateSessionId,
6
- type SessionContext,
7
- setDefaultSiteKey,
8
- } from "../src/utils/session-manager";
9
-
10
- function decodeBody(data: string | Uint8Array): string {
11
- if (typeof data === "string") return data;
12
- return gunzipSync(data).toString("utf8");
13
- }
14
-
15
- class MockStorage {
16
- private readonly data = new Map<string, string>();
17
-
18
- getItem(key: string): string | null {
19
- return this.data.get(key) ?? null;
20
- }
21
-
22
- setItem(key: string, value: string): void {
23
- this.data.set(key, value);
24
- }
25
-
26
- removeItem(key: string): void {
27
- this.data.delete(key);
28
- }
29
- }
30
-
31
- type ReplayEvent = {
32
- type: number;
33
- timestamp: number;
34
- data?: Record<string, unknown>;
35
- };
36
-
37
- type ReplayBatch = {
38
- token: string;
39
- sessionId: string;
40
- windowId: string;
41
- viewId: string;
42
- sessionStart: number;
43
- identifier?: string;
44
- batchId: string;
45
- sequence: number;
46
- timestamp: number;
47
- url: string;
48
- isFinal?: boolean;
49
- events: ReplayEvent[];
50
- };
51
-
52
- type ReplayTrackerInternals = {
53
- events: ReplayEvent[];
54
- pending: ReplayBatch[];
55
- pendingSizeBytes: number;
56
- minLengthFlushTask: ReturnType<typeof setTimeout> | null;
57
- retryTask: ReturnType<typeof setTimeout> | null;
58
- startTime: number;
59
- onEvent: (event: ReplayEvent, isCheckout?: boolean) => void;
60
- trackPageChange: (url?: string) => void;
61
- flush: (
62
- lowLatency: boolean,
63
- contextOverride?: SessionContext,
64
- isFinal?: boolean,
65
- ) => Promise<void>;
66
- send: (
67
- data: string | Uint8Array,
68
- isCompressed: boolean,
69
- lowLatency: boolean,
70
- ) => Promise<boolean>;
71
- };
72
-
73
- function setGlobal(name: keyof typeof globalThis, value: unknown): void {
74
- Object.defineProperty(globalThis, name, {
75
- configurable: true,
76
- writable: true,
77
- value,
78
- });
79
- }
80
-
81
- const original = {
82
- window: globalThis.window,
83
- document: globalThis.document,
84
- location: globalThis.location,
85
- navigator: globalThis.navigator,
86
- fetch: globalThis.fetch,
87
- localStorage: globalThis.localStorage,
88
- sessionStorage: globalThis.sessionStorage,
89
- };
90
-
91
- beforeEach(() => {
92
- setGlobal("window", {
93
- location: { href: "https://example.com/" },
94
- addEventListener: () => {},
95
- removeEventListener: () => {},
96
- } as unknown as Window);
97
- setGlobal("document", {
98
- visibilityState: "visible",
99
- addEventListener: () => {},
100
- removeEventListener: () => {},
101
- } as unknown as Document);
102
- setGlobal("location", { href: "https://example.com/" } as Location);
103
- setGlobal("localStorage", new MockStorage());
104
- setGlobal("sessionStorage", new MockStorage());
105
- setDefaultSiteKey("site_test");
106
- setGlobal("navigator", { sendBeacon: () => false });
107
- setGlobal(
108
- "fetch",
109
- (async () => new Response("", { status: 204 })) as unknown as typeof fetch,
110
- );
111
- });
112
-
113
- afterEach(() => {
114
- setGlobal("window", original.window);
115
- setGlobal("document", original.document);
116
- setGlobal("location", original.location);
117
- setGlobal("navigator", original.navigator);
118
- setGlobal("fetch", original.fetch);
119
- setGlobal("localStorage", original.localStorage);
120
- setGlobal("sessionStorage", original.sessionStorage);
121
- });
122
-
123
- function createTracker(
124
- options: Partial<ConstructorParameters<typeof ReplayTracker>[0]> = {},
125
- ): ReplayTrackerInternals {
126
- return new ReplayTracker({
127
- siteKey: "site_test",
128
- samplingPercentage: 100,
129
- ...options,
130
- }) as unknown as ReplayTrackerInternals;
131
- }
132
-
133
- async function captureReplayBatch(
134
- tracker: ReplayTrackerInternals,
135
- ): Promise<ReplayBatch> {
136
- const payloads: ReplayBatch[] = [];
137
- tracker.send = async (data) => {
138
- payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
139
- return true;
140
- };
141
- tracker.startTime = Date.now() - 1000;
142
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
143
- await tracker.flush(false);
144
- return payloads[0] as ReplayBatch;
145
- }
146
-
147
- describe("ReplayTracker", () => {
148
- test("buffers replay events", () => {
149
- const tracker = createTracker({
150
- maxEvents: 1000,
151
- minReplayLengthMs: Number.MAX_SAFE_INTEGER,
152
- });
153
-
154
- tracker.onEvent({ type: 2, timestamp: 1, data: {} }, false);
155
- tracker.onEvent({ type: 3, timestamp: 2, data: {} }, false);
156
-
157
- expect(tracker.events.length).toBe(2);
158
- });
159
-
160
- test("does not flush before minimum replay length", async () => {
161
- const tracker = createTracker({
162
- minReplayLengthMs: Number.MAX_SAFE_INTEGER,
163
- });
164
-
165
- let sent = 0;
166
- tracker.send = async () => {
167
- sent++;
168
- return true;
169
- };
170
- tracker.startTime = Date.now();
171
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
172
-
173
- await tracker.flush(false);
174
-
175
- expect(sent).toBe(0);
176
- expect(tracker.events.length).toBe(1);
177
- expect(tracker.pending.length).toBe(0);
178
-
179
- if (tracker.minLengthFlushTask) clearTimeout(tracker.minLengthFlushTask);
180
- });
181
-
182
- test("flushes buffered events once minimum replay length is reached", async () => {
183
- const tracker = createTracker({
184
- minReplayLengthMs: 20,
185
- });
186
-
187
- let sent = 0;
188
- tracker.send = async () => {
189
- sent++;
190
- return true;
191
- };
192
- tracker.startTime = Date.now();
193
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
194
-
195
- await new Promise((resolve) => setTimeout(resolve, 40));
196
-
197
- expect(sent).toBe(1);
198
- expect(tracker.events.length).toBe(0);
199
- expect(tracker.pending.length).toBe(0);
200
- });
201
-
202
- test("emits replay payload when flushed", async () => {
203
- const tracker = createTracker({
204
- minReplayLengthMs: 0,
205
- });
206
-
207
- const captured: { payload: ReplayBatch | null; lowLatency: boolean } = {
208
- payload: null,
209
- lowLatency: false,
210
- };
211
- tracker.send = async (data, _isCompressed, nextLowLatency) => {
212
- captured.payload = JSON.parse(decodeBody(data)) as ReplayBatch;
213
- captured.lowLatency = nextLowLatency;
214
- return true;
215
- };
216
- tracker.startTime = Date.now() - 1000;
217
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
218
-
219
- await tracker.flush(true);
220
-
221
- expect(captured.payload).not.toBeNull();
222
- expect(captured.payload?.token).toBe("site_test");
223
- expect(captured.payload?.sessionId).toBeTruthy();
224
- expect(captured.payload?.windowId).toBeTruthy();
225
- expect(captured.payload?.viewId).toBeTruthy();
226
- expect(captured.payload?.sessionStart).toBeGreaterThan(0);
227
- expect(captured.payload?.batchId).toBeTruthy();
228
- expect(captured.payload?.events).toHaveLength(1);
229
- expect(captured.lowLatency).toBe(true);
230
- expect(tracker.pending.length).toBe(0);
231
- });
232
-
233
- test("emits stable batch ids while queued for retry", async () => {
234
- const tracker = createTracker({
235
- minReplayLengthMs: 0,
236
- });
237
-
238
- const payloads: ReplayBatch[] = [];
239
- tracker.send = async (data) => {
240
- payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
241
- return payloads.length > 1;
242
- };
243
- tracker.startTime = Date.now() - 1000;
244
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
245
-
246
- await tracker.flush(false);
247
- await tracker.flush(false);
248
-
249
- expect(payloads).toHaveLength(2);
250
- expect(payloads[1]?.batchId).toBe(payloads[0]?.batchId);
251
- expect(payloads[1]?.sequence).toBe(payloads[0]?.sequence);
252
-
253
- if (tracker.retryTask) clearTimeout(tracker.retryTask);
254
- });
255
-
256
- test("keeps the same replay session id across later batches", async () => {
257
- const tracker = createTracker({
258
- minReplayLengthMs: 0,
259
- });
260
-
261
- const payloads: ReplayBatch[] = [];
262
- tracker.send = async (data) => {
263
- payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
264
- return true;
265
- };
266
-
267
- getOrCreateSessionId();
268
- tracker.startTime = Date.now() - 1000;
269
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
270
- await tracker.flush(false);
271
- tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
272
- await tracker.flush(false);
273
-
274
- expect(payloads).toHaveLength(2);
275
- expect(payloads[0]?.sessionId).toBeTruthy();
276
- expect(payloads[1]?.sessionId).toBe(payloads[0]?.sessionId);
277
- });
278
-
279
- test("uses a new session id after idle timeout", async () => {
280
- const tracker = createTracker({
281
- minReplayLengthMs: 0,
282
- });
283
-
284
- const payloads: ReplayBatch[] = [];
285
- tracker.send = async (data) => {
286
- payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
287
- return true;
288
- };
289
-
290
- getOrCreateSessionId();
291
- tracker.startTime = Date.now() - 1000;
292
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
293
- await tracker.flush(false);
294
-
295
- const local = globalThis.localStorage as unknown as MockStorage;
296
- local.setItem(
297
- "faststats_session_activity",
298
- (Date.now() - 31 * 60 * 1000).toString(),
299
- );
300
-
301
- tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
302
- await tracker.flush(false);
303
-
304
- tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
305
- await tracker.flush(false);
306
-
307
- expect(payloads).toHaveLength(3);
308
- expect(payloads[1]?.sessionId).toBe(payloads[0]?.sessionId);
309
- expect(payloads[1]?.isFinal).toBe(true);
310
- expect(payloads[2]?.sessionId).not.toBe(payloads[0]?.sessionId);
311
- });
312
-
313
- test("finalizes queued events under previous session after idle timeout", async () => {
314
- const tracker = createTracker({
315
- minReplayLengthMs: 0,
316
- });
317
-
318
- const payloads: ReplayBatch[] = [];
319
- tracker.send = async (data) => {
320
- payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
321
- return true;
322
- };
323
-
324
- const previousSessionId = getOrCreateSessionId();
325
- const local = globalThis.localStorage as unknown as MockStorage;
326
- local.setItem(
327
- "faststats_session_activity",
328
- (Date.now() - 31 * 60 * 1000).toString(),
329
- );
330
-
331
- tracker.startTime = Date.now() - 1000;
332
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
333
- await tracker.flush(false);
334
-
335
- expect(payloads).toHaveLength(1);
336
- expect(payloads[0]?.sessionId).toBe(previousSessionId);
337
- expect(payloads[0]?.isFinal).toBe(true);
338
- });
339
-
340
- test("keeps the same window and view ids across replay batches", async () => {
341
- const tracker = createTracker({
342
- minReplayLengthMs: 0,
343
- });
344
-
345
- const payloads: ReplayBatch[] = [];
346
- tracker.send = async (data) => {
347
- payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
348
- return true;
349
- };
350
- getOrCreateSessionId();
351
- tracker.startTime = Date.now() - 1000;
352
-
353
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
354
- await tracker.flush(false);
355
- tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
356
- await tracker.flush(false);
357
-
358
- expect(payloads).toHaveLength(2);
359
- expect(payloads[1]?.windowId).toBe(payloads[0]?.windowId);
360
- expect(payloads[1]?.viewId).toBe(payloads[0]?.viewId);
361
- expect(payloads[1]?.sequence).toBe((payloads[0]?.sequence ?? 0) + 1);
362
- });
363
-
364
- test("window id is stable across replay tracker remounts", async () => {
365
- const sharedSession = new MockStorage();
366
- sharedSession.setItem("faststats_window_id_site_test", "stable-window");
367
-
368
- setGlobal("sessionStorage", sharedSession);
369
-
370
- const first = createTracker({ minReplayLengthMs: 0 });
371
- const second = createTracker({ minReplayLengthMs: 0 });
372
-
373
- const firstBatch = await captureReplayBatch(first);
374
- const secondBatch = await captureReplayBatch(second);
375
-
376
- expect(firstBatch.windowId).toBe("stable-window");
377
- expect(secondBatch.windowId).toBe("stable-window");
378
- });
379
-
380
- test("same session across tabs with different window ids", async () => {
381
- const sharedLocal = new MockStorage();
382
- sharedLocal.setItem("faststats_session_id", "shared-session");
383
- sharedLocal.setItem("faststats_session_activity", Date.now().toString());
384
- sharedLocal.setItem("faststats_session_start", Date.now().toString());
385
-
386
- setGlobal("localStorage", sharedLocal);
387
- setGlobal("sessionStorage", new MockStorage());
388
-
389
- const first = createTracker({ minReplayLengthMs: 0 });
390
- const firstBatch = await captureReplayBatch(first);
391
-
392
- setGlobal("sessionStorage", new MockStorage());
393
- const second = createTracker({ minReplayLengthMs: 0 });
394
- const secondBatch = await captureReplayBatch(second);
395
-
396
- expect(firstBatch.sessionId).toBe("shared-session");
397
- expect(secondBatch.sessionId).toBe("shared-session");
398
- expect(firstBatch.windowId).not.toBe(secondBatch.windowId);
399
- });
400
-
401
- test("trackPageChange rotates viewId and emits meta event", () => {
402
- const tracker = createTracker();
403
- (tracker as unknown as { started: boolean }).started = true;
404
- const firstViewId = (tracker as unknown as { viewId: string }).viewId;
405
-
406
- tracker.trackPageChange("https://example.com/about");
407
-
408
- const viewEvent = tracker.events.find(
409
- (e) =>
410
- (e.data as Record<string, unknown> | undefined)?.tag ===
411
- "faststats:view",
412
- );
413
- expect(viewEvent).toBeDefined();
414
- const payload = (viewEvent?.data as Record<string, unknown> | undefined)
415
- ?.payload as Record<string, unknown> | undefined;
416
- expect(payload?.href).toBe("https://example.com/about");
417
- expect((tracker as unknown as { viewId: string }).viewId).not.toBe(
418
- firstViewId,
419
- );
420
- });
421
-
422
- test("sends replay as plain JSON when CompressionStream is unavailable", async () => {
423
- const tracker = createTracker({
424
- minReplayLengthMs: 0,
425
- });
426
-
427
- const captured: { bodyType: "string" | "blob" | null } = {
428
- bodyType: null,
429
- };
430
- tracker.send = async (data) => {
431
- captured.bodyType = typeof data === "string" ? "string" : "blob";
432
- return true;
433
- };
434
- tracker.startTime = Date.now() - 1000;
435
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
436
-
437
- await tracker.flush(false);
438
-
439
- expect(captured.bodyType).toBe("string");
440
- });
441
-
442
- test("sends replay as gzip Uint8Array when CompressionStream is available", async () => {
443
- setGlobal("window", {
444
- ...(globalThis.window as unknown as Record<string, unknown>),
445
- CompressionStream: globalThis.CompressionStream,
446
- });
447
-
448
- const tracker = createTracker({
449
- minReplayLengthMs: 0,
450
- });
451
-
452
- const captured: {
453
- bodyType: "string" | "binary" | null;
454
- compressed: boolean;
455
- size: number;
456
- } = { bodyType: null, compressed: false, size: 0 };
457
- tracker.send = async (data, isCompressed) => {
458
- captured.bodyType = typeof data === "string" ? "string" : "binary";
459
- captured.compressed = isCompressed;
460
- if (data instanceof Uint8Array) {
461
- captured.size = data.byteLength;
462
- }
463
- return true;
464
- };
465
- tracker.startTime = Date.now() - 1000;
466
- for (let i = 0; i < 10; i++) {
467
- tracker.onEvent(
468
- {
469
- type: 2,
470
- timestamp: Date.now(),
471
- data: { i, payload: "x".repeat(500) },
472
- },
473
- false,
474
- );
475
- }
476
-
477
- await tracker.flush(false);
478
-
479
- expect(captured.bodyType).toBe("binary");
480
- expect(captured.compressed).toBe(true);
481
- expect(captured.size).toBeGreaterThan(0);
482
- expect(captured.size).toBeLessThan(5000);
483
- });
484
-
485
- test("compresses on unload (low latency) flush", async () => {
486
- setGlobal("window", {
487
- ...(globalThis.window as unknown as Record<string, unknown>),
488
- CompressionStream: globalThis.CompressionStream,
489
- });
490
-
491
- const tracker = createTracker({
492
- minReplayLengthMs: 0,
493
- });
494
-
495
- let compressedFlag = false;
496
- let lowLatencyFlag = false;
497
- tracker.send = async (_data, isCompressed, lowLatency) => {
498
- compressedFlag = isCompressed;
499
- lowLatencyFlag = lowLatency;
500
- return true;
501
- };
502
- tracker.startTime = Date.now() - 1000;
503
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
504
-
505
- await tracker.flush(true, undefined, true);
506
-
507
- expect(compressedFlag).toBe(true);
508
- expect(lowLatencyFlag).toBe(true);
509
- });
510
-
511
- test("keeps failed batches queued for retry", async () => {
512
- const tracker = createTracker({
513
- minReplayLengthMs: 0,
514
- });
515
-
516
- tracker.send = async () => false;
517
- tracker.startTime = Date.now() - 1000;
518
- tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
519
-
520
- await tracker.flush(false);
521
-
522
- expect(tracker.pending.length).toBe(1);
523
- expect(tracker.retryTask).not.toBeNull();
524
-
525
- if (tracker.retryTask) clearTimeout(tracker.retryTask);
526
- });
527
-
528
- test("drops replay batches that exceed the queue byte limit", async () => {
529
- const tracker = createTracker({
530
- minReplayLengthMs: 0,
531
- maxQueueSizeBytes: 600,
532
- });
533
-
534
- let sent = 0;
535
- tracker.send = async () => {
536
- sent++;
537
- return true;
538
- };
539
- tracker.startTime = Date.now() - 1000;
540
- tracker.onEvent(
541
- {
542
- type: 2,
543
- timestamp: Date.now(),
544
- data: { payload: "x".repeat(1000) },
545
- },
546
- false,
547
- );
548
-
549
- await tracker.flush(false);
550
-
551
- expect(sent).toBe(0);
552
- expect(tracker.pending.length).toBe(0);
553
- expect(tracker.pendingSizeBytes).toBe(0);
554
- });
555
-
556
- test("keeps pending replay queue under the byte limit", async () => {
557
- const tracker = createTracker({
558
- minReplayLengthMs: 0,
559
- maxQueueSizeBytes: 900,
560
- });
561
-
562
- tracker.send = async () => false;
563
- tracker.startTime = Date.now() - 1000;
564
-
565
- for (let i = 0; i < 4; i++) {
566
- tracker.onEvent(
567
- {
568
- type: 2,
569
- timestamp: Date.now(),
570
- data: { i, payload: "x".repeat(300) },
571
- },
572
- false,
573
- );
574
- await tracker.flush(false);
575
- }
576
-
577
- expect(tracker.pending.length).toBeGreaterThan(0);
578
- expect(tracker.pendingSizeBytes).toBeLessThanOrEqual(900);
579
-
580
- if (tracker.retryTask) clearTimeout(tracker.retryTask);
581
- });
582
- });