@dash0/sdk-web 0.18.2 → 0.18.4
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 +3 -3
- package/dist/dash0.iife.js +1 -1
- package/dist/dash0.iife.js.map +1 -1
- package/dist/dash0.js +1 -1
- package/dist/dash0.js.map +1 -1
- package/dist/dash0.umd.cjs +1 -1
- package/dist/dash0.umd.cjs.map +1 -1
- package/dist/modules/transport/fetch.js +14 -2
- package/dist/modules/transport/fetch_test.js +267 -0
- package/dist/modules/utils/time.js +11 -2
- package/dist/modules/utils/time_test.js +232 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/transport/fetch_test.d.ts +1 -0
- package/dist/types/utils/time_test.d.ts +1 -0
- package/package.json +1 -1
- package/src/transport/fetch.ts +13 -2
- package/src/transport/fetch_test.ts +337 -0
- package/src/utils/time.ts +13 -2
- package/src/utils/time_test.ts +282 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { send } from "./fetch";
|
|
3
|
+
import { vars } from "../vars";
|
|
4
|
+
import * as utils from "../utils";
|
|
5
|
+
|
|
6
|
+
describe("fetch transport keepalive handling", () => {
|
|
7
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
8
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Reset vars
|
|
12
|
+
vars.endpoints = [
|
|
13
|
+
{
|
|
14
|
+
url: "https://api.example.com/",
|
|
15
|
+
authToken: "test-token",
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
vars.enableTransportCompression = false;
|
|
19
|
+
|
|
20
|
+
// Mock fetch with proper response
|
|
21
|
+
fetchMock = vi.fn().mockResolvedValue({
|
|
22
|
+
ok: true,
|
|
23
|
+
status: 200,
|
|
24
|
+
statusText: "OK",
|
|
25
|
+
text: vi.fn().mockResolvedValue(""),
|
|
26
|
+
});
|
|
27
|
+
vi.spyOn(utils, "fetch" as any).mockImplementation(fetchMock);
|
|
28
|
+
|
|
29
|
+
// Spy on warn to verify error handling
|
|
30
|
+
consoleWarnSpy = vi.spyOn(utils, "warn").mockImplementation(() => {});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should set keepalive=true for small single request", async () => {
|
|
38
|
+
const smallBody = { data: "test" };
|
|
39
|
+
|
|
40
|
+
await send("/v1/traces", smallBody);
|
|
41
|
+
|
|
42
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
43
|
+
const fetchOptions = fetchMock.mock.calls[0]![1];
|
|
44
|
+
expect(fetchOptions.keepalive).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should set keepalive=false when body size exceeds BEACON_BODY_SIZE_LIMIT", async () => {
|
|
48
|
+
// Create a large body that exceeds 60KB
|
|
49
|
+
const largeBody = { data: "x".repeat(65000) };
|
|
50
|
+
|
|
51
|
+
await send("/v1/traces", largeBody);
|
|
52
|
+
|
|
53
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
54
|
+
const fetchOptions = fetchMock.mock.calls[0]![1];
|
|
55
|
+
expect(fetchOptions.keepalive).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should handle multiple parallel requests within limits", async () => {
|
|
59
|
+
const smallBody = { data: "test" };
|
|
60
|
+
|
|
61
|
+
// Send 3 requests in parallel
|
|
62
|
+
await Promise.all([send("/v1/traces", smallBody), send("/v1/traces", smallBody), send("/v1/traces", smallBody)]);
|
|
63
|
+
|
|
64
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
65
|
+
fetchMock.mock.calls.forEach((call) => {
|
|
66
|
+
expect(call[1].keepalive).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should set keepalive=false when pending request count exceeds 15", async () => {
|
|
71
|
+
// Create 16 endpoints to trigger the limit
|
|
72
|
+
vars.endpoints = Array.from({ length: 16 }, (_, i) => ({
|
|
73
|
+
url: `https://api${i}.example.com/`,
|
|
74
|
+
authToken: `token-${i}`,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const smallBody = { data: "test" };
|
|
78
|
+
|
|
79
|
+
// Mock fetch to delay response to keep requests pending
|
|
80
|
+
let resolvers: Array<() => void> = [];
|
|
81
|
+
fetchMock.mockImplementation(
|
|
82
|
+
() =>
|
|
83
|
+
new Promise((resolve) => {
|
|
84
|
+
resolvers.push(() =>
|
|
85
|
+
resolve({
|
|
86
|
+
ok: true,
|
|
87
|
+
status: 200,
|
|
88
|
+
statusText: "OK",
|
|
89
|
+
text: vi.fn().mockResolvedValue(""),
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const sendPromise = send("/v1/traces", smallBody);
|
|
96
|
+
|
|
97
|
+
// Wait a bit for all requests to be initiated
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
99
|
+
|
|
100
|
+
// Check that at least one request has keepalive=false
|
|
101
|
+
const keepaliveValues = fetchMock.mock.calls.map((call) => call[1].keepalive);
|
|
102
|
+
expect(keepaliveValues.some((v) => v === false)).toBe(true);
|
|
103
|
+
|
|
104
|
+
// Resolve all requests
|
|
105
|
+
resolvers.forEach((resolve) => resolve());
|
|
106
|
+
await sendPromise;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should correctly adjust pendingBodySize when request succeeds", async () => {
|
|
110
|
+
// Create bodies that are ~35KB each, so two together would exceed the 60KB limit
|
|
111
|
+
const mediumBody1 = { data: "x".repeat(35000) };
|
|
112
|
+
const mediumBody2 = { data: "y".repeat(35000) };
|
|
113
|
+
|
|
114
|
+
// First request: pendingBodySize starts at 0, so keepalive should be true
|
|
115
|
+
await send("/v1/traces", mediumBody1);
|
|
116
|
+
expect(fetchMock.mock.calls[0]![1].keepalive).toBe(true);
|
|
117
|
+
|
|
118
|
+
// After first request completes, pendingBodySize should be back to 0
|
|
119
|
+
// Second request: pendingBodySize starts at 0 again, so keepalive should be true
|
|
120
|
+
await send("/v1/traces", mediumBody2);
|
|
121
|
+
expect(fetchMock.mock.calls[1]![1].keepalive).toBe(true);
|
|
122
|
+
|
|
123
|
+
// This verifies that the pendingBodySize is properly decremented in the finally block
|
|
124
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should correctly adjust pendingBodySize when request fails with network error", async () => {
|
|
128
|
+
// Create bodies that are ~35KB each, so two together would exceed the 60KB limit
|
|
129
|
+
const mediumBody = { data: "x".repeat(35000) };
|
|
130
|
+
|
|
131
|
+
// First request fails
|
|
132
|
+
fetchMock.mockRejectedValueOnce(new Error("Network error"));
|
|
133
|
+
// Second request succeeds
|
|
134
|
+
fetchMock.mockResolvedValueOnce({
|
|
135
|
+
ok: true,
|
|
136
|
+
status: 200,
|
|
137
|
+
statusText: "OK",
|
|
138
|
+
text: vi.fn().mockResolvedValue(""),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// First request: should have keepalive=true (pendingBodySize is 0)
|
|
142
|
+
await send("/v1/traces", mediumBody);
|
|
143
|
+
expect(fetchMock.mock.calls[0]![1].keepalive).toBe(true);
|
|
144
|
+
|
|
145
|
+
// Even though first request failed, pendingBodySize should be decremented in finally block
|
|
146
|
+
// Second request: should also have keepalive=true (pendingBodySize is back to 0)
|
|
147
|
+
await send("/v1/traces", mediumBody);
|
|
148
|
+
expect(fetchMock.mock.calls[1]![1].keepalive).toBe(true);
|
|
149
|
+
|
|
150
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
151
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Error sending telemetry"), expect.any(Error));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should correctly adjust pendingBodySize when response.text() fails", async () => {
|
|
155
|
+
// Create bodies that are ~35KB each, so two together would exceed the 60KB limit
|
|
156
|
+
const mediumBody = { data: "x".repeat(35000) };
|
|
157
|
+
|
|
158
|
+
// Mock response.text() to fail
|
|
159
|
+
fetchMock.mockResolvedValueOnce({
|
|
160
|
+
ok: true,
|
|
161
|
+
status: 200,
|
|
162
|
+
statusText: "OK",
|
|
163
|
+
text: vi.fn().mockRejectedValue(new Error("Failed to read body")),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// First request: should have keepalive=true (pendingBodySize is 0)
|
|
167
|
+
await send("/v1/traces", mediumBody);
|
|
168
|
+
expect(fetchMock.mock.calls[0]![1].keepalive).toBe(true);
|
|
169
|
+
|
|
170
|
+
// Should still complete successfully and adjust pendingBodySize even though text() failed
|
|
171
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
172
|
+
|
|
173
|
+
// Next request should still work with keepalive=true (verifying pendingBodySize is 0)
|
|
174
|
+
fetchMock.mockResolvedValueOnce({
|
|
175
|
+
ok: true,
|
|
176
|
+
status: 200,
|
|
177
|
+
statusText: "OK",
|
|
178
|
+
text: vi.fn().mockResolvedValue(""),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await send("/v1/traces", mediumBody);
|
|
182
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
183
|
+
expect(fetchMock.mock.calls[1]![1].keepalive).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should handle multiple parallel requests with failures", async () => {
|
|
187
|
+
vars.endpoints = [
|
|
188
|
+
{ url: "https://api1.example.com/", authToken: "token1" },
|
|
189
|
+
{ url: "https://api2.example.com/", authToken: "token2" },
|
|
190
|
+
{ url: "https://api3.example.com/", authToken: "token3" },
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const body = { data: "test" };
|
|
194
|
+
|
|
195
|
+
// First endpoint fails, others succeed
|
|
196
|
+
fetchMock
|
|
197
|
+
.mockRejectedValueOnce(new Error("Network error"))
|
|
198
|
+
.mockResolvedValueOnce({
|
|
199
|
+
ok: true,
|
|
200
|
+
status: 200,
|
|
201
|
+
statusText: "OK",
|
|
202
|
+
text: vi.fn().mockResolvedValue(""),
|
|
203
|
+
})
|
|
204
|
+
.mockResolvedValueOnce({
|
|
205
|
+
ok: true,
|
|
206
|
+
status: 200,
|
|
207
|
+
statusText: "OK",
|
|
208
|
+
text: vi.fn().mockResolvedValue(""),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await send("/v1/traces", body);
|
|
212
|
+
|
|
213
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
214
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Error sending telemetry"), expect.any(Error));
|
|
215
|
+
|
|
216
|
+
// After all complete, counters should be reset
|
|
217
|
+
// Reset endpoints back to 1
|
|
218
|
+
vars.endpoints = [
|
|
219
|
+
{
|
|
220
|
+
url: "https://api.example.com/",
|
|
221
|
+
authToken: "test-token",
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
fetchMock.mockResolvedValueOnce({
|
|
226
|
+
ok: true,
|
|
227
|
+
status: 200,
|
|
228
|
+
statusText: "OK",
|
|
229
|
+
text: vi.fn().mockResolvedValue(""),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await send("/v1/traces", body);
|
|
233
|
+
expect(fetchMock).toHaveBeenCalledTimes(4);
|
|
234
|
+
expect(fetchMock.mock.calls[3]![1].keepalive).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should correctly handle mixed success and failure in parallel with size tracking", async () => {
|
|
238
|
+
vars.endpoints = [
|
|
239
|
+
{ url: "https://api1.example.com/", authToken: "token1" },
|
|
240
|
+
{ url: "https://api2.example.com/", authToken: "token2" },
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
// Create a body that's about 30KB
|
|
244
|
+
const mediumBody = { data: "x".repeat(30000) };
|
|
245
|
+
|
|
246
|
+
// First endpoint fails, second succeeds
|
|
247
|
+
fetchMock.mockRejectedValueOnce(new Error("Network error")).mockResolvedValueOnce({
|
|
248
|
+
ok: true,
|
|
249
|
+
status: 200,
|
|
250
|
+
statusText: "OK",
|
|
251
|
+
text: vi.fn().mockResolvedValue(""),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await send("/v1/traces", mediumBody);
|
|
255
|
+
|
|
256
|
+
// Both requests should have been made
|
|
257
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
258
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Error sending telemetry"), expect.any(Error));
|
|
259
|
+
|
|
260
|
+
// After completion, pendingBodySize should be back to 0
|
|
261
|
+
// Reset to single endpoint for simpler test
|
|
262
|
+
vars.endpoints = [
|
|
263
|
+
{
|
|
264
|
+
url: "https://api.example.com/",
|
|
265
|
+
authToken: "test-token",
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
fetchMock.mockResolvedValueOnce({
|
|
270
|
+
ok: true,
|
|
271
|
+
status: 200,
|
|
272
|
+
statusText: "OK",
|
|
273
|
+
text: vi.fn().mockResolvedValue(""),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await send("/v1/traces", { data: "test" });
|
|
277
|
+
expect(fetchMock.mock.calls[2]![1].keepalive).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should handle requests with non-ok response status", async () => {
|
|
281
|
+
// Create bodies that are ~35KB each, so two together would exceed the 60KB limit
|
|
282
|
+
const mediumBody = { data: "x".repeat(35000) };
|
|
283
|
+
|
|
284
|
+
fetchMock.mockResolvedValueOnce({
|
|
285
|
+
ok: false,
|
|
286
|
+
status: 500,
|
|
287
|
+
statusText: "Internal Server Error",
|
|
288
|
+
text: vi.fn().mockResolvedValue(""),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// First request with error response
|
|
292
|
+
await send("/v1/traces", mediumBody);
|
|
293
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
294
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to send telemetry"));
|
|
295
|
+
|
|
296
|
+
// Verify pendingBodySize was adjusted correctly by sending another request
|
|
297
|
+
fetchMock.mockResolvedValueOnce({
|
|
298
|
+
ok: true,
|
|
299
|
+
status: 200,
|
|
300
|
+
statusText: "OK",
|
|
301
|
+
text: vi.fn().mockResolvedValue(""),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Second request should have keepalive=true (verifying pendingBodySize was decremented)
|
|
305
|
+
await send("/v1/traces", mediumBody);
|
|
306
|
+
expect(fetchMock.mock.calls[1]![1].keepalive).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should handle the exact boundary of BEACON_BODY_SIZE_LIMIT", async () => {
|
|
310
|
+
// Create a body that's just under 60000 bytes
|
|
311
|
+
const nearBoundaryBody = { data: "x".repeat(59970) }; // Accounting for JSON overhead
|
|
312
|
+
|
|
313
|
+
await send("/v1/traces", nearBoundaryBody);
|
|
314
|
+
|
|
315
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
316
|
+
const fetchOptions = fetchMock.mock.calls[0]![1];
|
|
317
|
+
|
|
318
|
+
// Should be true since we're under the limit
|
|
319
|
+
expect(fetchOptions.keepalive).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should handle exactly 15 parallel requests", async () => {
|
|
323
|
+
vars.endpoints = Array.from({ length: 15 }, (_, i) => ({
|
|
324
|
+
url: `https://api${i}.example.com/`,
|
|
325
|
+
authToken: `token-${i}`,
|
|
326
|
+
}));
|
|
327
|
+
|
|
328
|
+
const smallBody = { data: "test" };
|
|
329
|
+
|
|
330
|
+
await send("/v1/traces", smallBody);
|
|
331
|
+
|
|
332
|
+
expect(fetchMock).toHaveBeenCalledTimes(15);
|
|
333
|
+
fetchMock.mock.calls.forEach((call) => {
|
|
334
|
+
expect(call[1].keepalive).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
package/src/utils/time.ts
CHANGED
|
@@ -8,11 +8,22 @@ export function now(): number {
|
|
|
8
8
|
|
|
9
9
|
export function nowNanos(): string {
|
|
10
10
|
const timeOrigin = getTimeOrigin();
|
|
11
|
+
const currentDate = new Date();
|
|
12
|
+
|
|
11
13
|
if (timeOrigin) {
|
|
12
|
-
|
|
14
|
+
const perfTime = perf.now() + timeOrigin;
|
|
15
|
+
const dateTime = currentDate.getTime();
|
|
16
|
+
|
|
17
|
+
// Only return perf time measurement if it hasn't significantly drifted (60s).
|
|
18
|
+
// Some browsers violate the performance api spec and do not tick the monotonic time while the OS sleeps or the browser hangs
|
|
19
|
+
// See https://bugzil.la/1709767
|
|
20
|
+
// See https://webkit.org/b/225610
|
|
21
|
+
if (Math.abs(perfTime - dateTime) < 60000) {
|
|
22
|
+
return String(perfTime * 1000000);
|
|
23
|
+
}
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
return toNanosTs(
|
|
26
|
+
return toNanosTs(currentDate);
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
export function toNanosTs(time: TimeInput): string {
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { expect, describe, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { now, nowNanos, toNanosTs, getTimeOrigin, domHRTimestampToNanos } from "./time";
|
|
3
|
+
import * as globals from "./globals";
|
|
4
|
+
|
|
5
|
+
describe("now", () => {
|
|
6
|
+
it("returns the current time in milliseconds", () => {
|
|
7
|
+
const before = new Date().getTime();
|
|
8
|
+
const result = now();
|
|
9
|
+
const after = new Date().getTime();
|
|
10
|
+
|
|
11
|
+
expect(result).toBeGreaterThanOrEqual(before);
|
|
12
|
+
expect(result).toBeLessThanOrEqual(after);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("toNanosTs", () => {
|
|
17
|
+
it("converts milliseconds to nanoseconds as string", () => {
|
|
18
|
+
const ms = 1609459200000; // 2021-01-01 00:00:00 UTC
|
|
19
|
+
const result = toNanosTs(ms);
|
|
20
|
+
expect(result).toBe("1609459200000000000");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("converts Date object to nanoseconds as string", () => {
|
|
24
|
+
const date = new Date(1609459200000);
|
|
25
|
+
const result = toNanosTs(date);
|
|
26
|
+
expect(result).toBe("1609459200000000000");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("handles zero timestamp", () => {
|
|
30
|
+
const result = toNanosTs(0);
|
|
31
|
+
expect(result).toBe("0000000");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("preserves precision", () => {
|
|
35
|
+
const ms = 1234567890123;
|
|
36
|
+
const result = toNanosTs(ms);
|
|
37
|
+
expect(result).toBe("1234567890123000000");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("getTimeOrigin", () => {
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns timeOrigin when available", () => {
|
|
47
|
+
const mockPerf = {
|
|
48
|
+
timeOrigin: 1609459200000,
|
|
49
|
+
now: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
52
|
+
|
|
53
|
+
const result = getTimeOrigin();
|
|
54
|
+
expect(result).toBe(1609459200000);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("falls back to timing.fetchStart when timeOrigin is not a number", () => {
|
|
58
|
+
const mockPerf = {
|
|
59
|
+
timing: {
|
|
60
|
+
fetchStart: 1609459200000,
|
|
61
|
+
},
|
|
62
|
+
now: vi.fn(),
|
|
63
|
+
};
|
|
64
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
65
|
+
|
|
66
|
+
const result = getTimeOrigin();
|
|
67
|
+
expect(result).toBe(1609459200000);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns undefined when perf is not available", () => {
|
|
71
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(undefined);
|
|
72
|
+
|
|
73
|
+
const result = getTimeOrigin();
|
|
74
|
+
expect(result).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns undefined when both timeOrigin and fetchStart are unavailable", () => {
|
|
78
|
+
const mockPerf = {
|
|
79
|
+
now: vi.fn(),
|
|
80
|
+
};
|
|
81
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
82
|
+
|
|
83
|
+
const result = getTimeOrigin();
|
|
84
|
+
expect(result).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("nowNanos", () => {
|
|
89
|
+
let dateNowSpy: any;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
dateNowSpy = vi.spyOn(Date.prototype, "getTime");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
vi.restoreAllMocks();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("uses performance time when available and not drifted", () => {
|
|
100
|
+
const timeOrigin = 1609459200000;
|
|
101
|
+
const perfNow = 5000; // 5 seconds after time origin
|
|
102
|
+
const expectedTime = timeOrigin + perfNow; // 1609459205000
|
|
103
|
+
|
|
104
|
+
const mockPerf = {
|
|
105
|
+
timeOrigin,
|
|
106
|
+
now: vi.fn().mockReturnValue(perfNow),
|
|
107
|
+
};
|
|
108
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
109
|
+
dateNowSpy.mockReturnValue(expectedTime);
|
|
110
|
+
|
|
111
|
+
const result = nowNanos();
|
|
112
|
+
const expected = String(expectedTime * 1000000);
|
|
113
|
+
expect(result).toBe(expected);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("falls back to Date when drift is exactly 60000ms (60 seconds)", () => {
|
|
117
|
+
const timeOrigin = 1609459200000;
|
|
118
|
+
const perfNow = 5000;
|
|
119
|
+
const perfTime = timeOrigin + perfNow; // 1609459205000
|
|
120
|
+
const dateTime = perfTime + 60000; // 60000ms drift
|
|
121
|
+
|
|
122
|
+
const mockPerf = {
|
|
123
|
+
timeOrigin,
|
|
124
|
+
now: vi.fn().mockReturnValue(perfNow),
|
|
125
|
+
};
|
|
126
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
127
|
+
dateNowSpy.mockReturnValue(dateTime);
|
|
128
|
+
|
|
129
|
+
const result = nowNanos();
|
|
130
|
+
const expected = String(dateTime) + "000000";
|
|
131
|
+
expect(result).toBe(expected);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("falls back to Date when drift exceeds 60000ms (positive)", () => {
|
|
135
|
+
const timeOrigin = 1609459200000;
|
|
136
|
+
const perfNow = 5000;
|
|
137
|
+
const perfTime = timeOrigin + perfNow;
|
|
138
|
+
const dateTime = perfTime + 65000; // 65000ms drift (browser slept)
|
|
139
|
+
|
|
140
|
+
const mockPerf = {
|
|
141
|
+
timeOrigin,
|
|
142
|
+
now: vi.fn().mockReturnValue(perfNow),
|
|
143
|
+
};
|
|
144
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
145
|
+
dateNowSpy.mockReturnValue(dateTime);
|
|
146
|
+
|
|
147
|
+
const result = nowNanos();
|
|
148
|
+
const expected = String(dateTime) + "000000";
|
|
149
|
+
expect(result).toBe(expected);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("falls back to Date when drift exceeds 60000ms (negative)", () => {
|
|
153
|
+
const timeOrigin = 1609459200000;
|
|
154
|
+
const perfNow = 5000;
|
|
155
|
+
const perfTime = timeOrigin + perfNow;
|
|
156
|
+
const dateTime = perfTime - 65000; // -65000ms drift (clock adjustment)
|
|
157
|
+
|
|
158
|
+
const mockPerf = {
|
|
159
|
+
timeOrigin,
|
|
160
|
+
now: vi.fn().mockReturnValue(perfNow),
|
|
161
|
+
};
|
|
162
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
163
|
+
dateNowSpy.mockReturnValue(dateTime);
|
|
164
|
+
|
|
165
|
+
const result = nowNanos();
|
|
166
|
+
const expected = String(dateTime) + "000000";
|
|
167
|
+
expect(result).toBe(expected);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("uses performance time when drift is just under 60000ms (59999ms)", () => {
|
|
171
|
+
const timeOrigin = 1609459200000;
|
|
172
|
+
const perfNow = 5000;
|
|
173
|
+
const perfTime = timeOrigin + perfNow;
|
|
174
|
+
const dateTime = perfTime + 59999; // 59999ms drift (still acceptable)
|
|
175
|
+
|
|
176
|
+
const mockPerf = {
|
|
177
|
+
timeOrigin,
|
|
178
|
+
now: vi.fn().mockReturnValue(perfNow),
|
|
179
|
+
};
|
|
180
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
181
|
+
dateNowSpy.mockReturnValue(dateTime);
|
|
182
|
+
|
|
183
|
+
const result = nowNanos();
|
|
184
|
+
const expected = String(perfTime * 1000000);
|
|
185
|
+
expect(result).toBe(expected);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("uses performance time when drift is just under -60000ms (-59999ms)", () => {
|
|
189
|
+
const timeOrigin = 1609459200000;
|
|
190
|
+
const perfNow = 5000;
|
|
191
|
+
const perfTime = timeOrigin + perfNow;
|
|
192
|
+
const dateTime = perfTime - 59999; // -59999ms drift (still acceptable)
|
|
193
|
+
|
|
194
|
+
const mockPerf = {
|
|
195
|
+
timeOrigin,
|
|
196
|
+
now: vi.fn().mockReturnValue(perfNow),
|
|
197
|
+
};
|
|
198
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
199
|
+
dateNowSpy.mockReturnValue(dateTime);
|
|
200
|
+
|
|
201
|
+
const result = nowNanos();
|
|
202
|
+
const expected = String(perfTime * 1000000);
|
|
203
|
+
expect(result).toBe(expected);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("falls back to Date when timeOrigin is not available", () => {
|
|
207
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(undefined);
|
|
208
|
+
const dateTime = 1609459205000;
|
|
209
|
+
dateNowSpy.mockReturnValue(dateTime);
|
|
210
|
+
|
|
211
|
+
const result = nowNanos();
|
|
212
|
+
const expected = String(dateTime) + "000000";
|
|
213
|
+
expect(result).toBe(expected);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("falls back to Date when timeOrigin is not a number", () => {
|
|
217
|
+
const mockPerf = {
|
|
218
|
+
timeOrigin: undefined,
|
|
219
|
+
now: vi.fn().mockReturnValue(5000),
|
|
220
|
+
timing: {}, // no fetchStart either
|
|
221
|
+
};
|
|
222
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
223
|
+
const dateTime = 1609459205000;
|
|
224
|
+
dateNowSpy.mockReturnValue(dateTime);
|
|
225
|
+
|
|
226
|
+
const result = nowNanos();
|
|
227
|
+
const expected = String(dateTime) + "000000";
|
|
228
|
+
expect(result).toBe(expected);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("domHRTimestampToNanos", () => {
|
|
233
|
+
afterEach(() => {
|
|
234
|
+
vi.restoreAllMocks();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("converts DOM high resolution timestamp to nanoseconds", () => {
|
|
238
|
+
const timeOrigin = 1609459200000;
|
|
239
|
+
const hrTimestamp = 5000.123; // 5.000123 seconds after time origin
|
|
240
|
+
|
|
241
|
+
const mockPerf = {
|
|
242
|
+
timeOrigin,
|
|
243
|
+
now: vi.fn(),
|
|
244
|
+
};
|
|
245
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
246
|
+
|
|
247
|
+
const result = domHRTimestampToNanos(hrTimestamp);
|
|
248
|
+
const expectedMs = timeOrigin + hrTimestamp;
|
|
249
|
+
const expectedNanos = String(Math.round(expectedMs * 1000000));
|
|
250
|
+
expect(result).toBe(expectedNanos);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("rounds to nearest nanosecond", () => {
|
|
254
|
+
const timeOrigin = 1000;
|
|
255
|
+
const hrTimestamp = 0.0000001; // Very small fraction
|
|
256
|
+
|
|
257
|
+
const mockPerf = {
|
|
258
|
+
timeOrigin,
|
|
259
|
+
now: vi.fn(),
|
|
260
|
+
};
|
|
261
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
262
|
+
|
|
263
|
+
const result = domHRTimestampToNanos(hrTimestamp);
|
|
264
|
+
const expectedMs = timeOrigin + hrTimestamp;
|
|
265
|
+
const expectedNanos = String(Math.round(expectedMs * 1000000));
|
|
266
|
+
expect(result).toBe(expectedNanos);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("handles zero timestamp", () => {
|
|
270
|
+
const timeOrigin = 1609459200000;
|
|
271
|
+
|
|
272
|
+
const mockPerf = {
|
|
273
|
+
timeOrigin,
|
|
274
|
+
now: vi.fn(),
|
|
275
|
+
};
|
|
276
|
+
vi.spyOn(globals, "perf", "get").mockReturnValue(mockPerf as any);
|
|
277
|
+
|
|
278
|
+
const result = domHRTimestampToNanos(0);
|
|
279
|
+
const expectedNanos = String(Math.round(timeOrigin * 1000000));
|
|
280
|
+
expect(result).toBe(expectedNanos);
|
|
281
|
+
});
|
|
282
|
+
});
|