@zuplo/cli 7.7.7 → 7.7.9
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/node_modules/@posthog/core/dist/error-tracking/index.d.ts +1 -1
- package/node_modules/@posthog/core/dist/error-tracking/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/error-tracking/parsers/base.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/error-tracking/parsers/base.js +19 -2
- package/node_modules/@posthog/core/dist/error-tracking/parsers/base.mjs +19 -2
- package/node_modules/@posthog/core/dist/index.d.ts +7 -0
- package/node_modules/@posthog/core/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/index.js +82 -21
- package/node_modules/@posthog/core/dist/index.mjs +4 -0
- package/node_modules/@posthog/core/dist/logs/index.d.ts +3 -2
- package/node_modules/@posthog/core/dist/logs/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/logs/index.js +28 -22
- package/node_modules/@posthog/core/dist/logs/index.mjs +29 -23
- package/node_modules/@posthog/core/dist/logs/logs-utils.js +1 -1
- package/node_modules/@posthog/core/dist/logs/logs-utils.mjs +2 -2
- package/node_modules/@posthog/core/dist/logs/types.d.ts +2 -2
- package/node_modules/@posthog/core/dist/metrics/index.d.ts +5 -2
- package/node_modules/@posthog/core/dist/metrics/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/metrics/index.js +28 -14
- package/node_modules/@posthog/core/dist/metrics/index.mjs +29 -15
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.js +1 -2
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.mjs +2 -3
- package/node_modules/@posthog/core/dist/metrics/types.d.ts +7 -0
- package/node_modules/@posthog/core/dist/metrics/types.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts +32 -4
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.js +68 -20
- package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +68 -20
- package/node_modules/@posthog/core/dist/traces/config.d.ts +13 -0
- package/node_modules/@posthog/core/dist/traces/config.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/config.js +101 -0
- package/node_modules/@posthog/core/dist/traces/config.mjs +63 -0
- package/node_modules/@posthog/core/dist/traces/context.d.ts +18 -0
- package/node_modules/@posthog/core/dist/traces/context.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/context.js +53 -0
- package/node_modules/@posthog/core/dist/traces/context.mjs +15 -0
- package/node_modules/@posthog/core/dist/traces/ids.d.ts +12 -0
- package/node_modules/@posthog/core/dist/traces/ids.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/ids.js +92 -0
- package/node_modules/@posthog/core/dist/traces/ids.mjs +42 -0
- package/node_modules/@posthog/core/dist/traces/index.d.ts +150 -0
- package/node_modules/@posthog/core/dist/traces/index.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/index.js +551 -0
- package/node_modules/@posthog/core/dist/traces/index.mjs +513 -0
- package/node_modules/@posthog/core/dist/traces/otlp.d.ts +24 -0
- package/node_modules/@posthog/core/dist/traces/otlp.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/otlp.js +158 -0
- package/node_modules/@posthog/core/dist/traces/otlp.mjs +108 -0
- package/node_modules/@posthog/core/dist/traces/sanitize.d.ts +32 -0
- package/node_modules/@posthog/core/dist/traces/sanitize.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/sanitize.js +93 -0
- package/node_modules/@posthog/core/dist/traces/sanitize.mjs +43 -0
- package/node_modules/@posthog/core/dist/traces/span.d.ts +189 -0
- package/node_modules/@posthog/core/dist/traces/span.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/span.js +533 -0
- package/node_modules/@posthog/core/dist/traces/span.mjs +462 -0
- package/node_modules/@posthog/core/dist/traces/traceparent.d.ts +51 -0
- package/node_modules/@posthog/core/dist/traces/traceparent.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/traceparent.js +116 -0
- package/node_modules/@posthog/core/dist/traces/traceparent.mjs +63 -0
- package/node_modules/@posthog/core/dist/traces/types.d.ts +114 -0
- package/node_modules/@posthog/core/dist/traces/types.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/types.js +18 -0
- package/node_modules/@posthog/core/dist/traces/types.mjs +1 -0
- package/node_modules/@posthog/core/dist/utils/backoff.d.ts +23 -0
- package/node_modules/@posthog/core/dist/utils/backoff.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/utils/backoff.js +65 -0
- package/node_modules/@posthog/core/dist/utils/backoff.mjs +14 -0
- package/node_modules/@posthog/core/dist/utils/flush-timer.d.ts +26 -0
- package/node_modules/@posthog/core/dist/utils/flush-timer.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/utils/flush-timer.js +67 -0
- package/node_modules/@posthog/core/dist/utils/flush-timer.mjs +29 -0
- package/node_modules/@posthog/core/dist/utils/json-utils.d.ts +7 -0
- package/node_modules/@posthog/core/dist/utils/json-utils.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/utils/json-utils.js +27 -0
- package/node_modules/@posthog/core/dist/utils/json-utils.mjs +25 -1
- package/node_modules/@posthog/core/dist/utils/otlp-resource.d.ts +13 -2
- package/node_modules/@posthog/core/dist/utils/otlp-resource.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/utils/otlp-resource.js +34 -7
- package/node_modules/@posthog/core/dist/utils/otlp-resource.mjs +26 -2
- package/node_modules/@posthog/core/dist/utils/retry-after.d.ts +55 -0
- package/node_modules/@posthog/core/dist/utils/retry-after.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/utils/retry-after.js +95 -0
- package/node_modules/@posthog/core/dist/utils/retry-after.mjs +50 -0
- package/node_modules/@posthog/core/package.json +3 -3
- package/node_modules/@posthog/core/src/error-tracking/index.ts +24 -1
- package/node_modules/@posthog/core/src/error-tracking/parsers/base.spec.ts +77 -7
- package/node_modules/@posthog/core/src/error-tracking/parsers/base.ts +42 -19
- package/node_modules/@posthog/core/src/index.ts +18 -0
- package/node_modules/@posthog/core/src/logs/index.spec.ts +490 -2
- package/node_modules/@posthog/core/src/logs/index.ts +63 -31
- package/node_modules/@posthog/core/src/logs/logs-utils.ts +2 -2
- package/node_modules/@posthog/core/src/logs/types.ts +2 -2
- package/node_modules/@posthog/core/src/metrics/index.spec.ts +304 -0
- package/node_modules/@posthog/core/src/metrics/index.ts +53 -16
- package/node_modules/@posthog/core/src/metrics/metrics-utils.ts +2 -3
- package/node_modules/@posthog/core/src/metrics/types.ts +10 -2
- package/node_modules/@posthog/core/src/posthog-core-stateless.ts +157 -38
- package/node_modules/@posthog/core/src/traces/config.spec.ts +271 -0
- package/node_modules/@posthog/core/src/traces/config.ts +142 -0
- package/node_modules/@posthog/core/src/traces/context.ts +30 -0
- package/node_modules/@posthog/core/src/traces/ids.spec.ts +115 -0
- package/node_modules/@posthog/core/src/traces/ids.ts +75 -0
- package/node_modules/@posthog/core/src/traces/index.spec.ts +3187 -0
- package/node_modules/@posthog/core/src/traces/index.ts +1060 -0
- package/node_modules/@posthog/core/src/traces/live-spans.spec.ts +69 -0
- package/node_modules/@posthog/core/src/traces/otlp.spec.ts +368 -0
- package/node_modules/@posthog/core/src/traces/otlp.ts +191 -0
- package/node_modules/@posthog/core/src/traces/sanitize.ts +116 -0
- package/node_modules/@posthog/core/src/traces/span.spec.ts +1122 -0
- package/node_modules/@posthog/core/src/traces/span.ts +822 -0
- package/node_modules/@posthog/core/src/traces/traceparent.spec.ts +184 -0
- package/node_modules/@posthog/core/src/traces/traceparent.ts +172 -0
- package/node_modules/@posthog/core/src/traces/types.ts +140 -0
- package/node_modules/@posthog/core/src/utils/backoff.ts +44 -0
- package/node_modules/@posthog/core/src/utils/flush-timer.ts +50 -0
- package/node_modules/@posthog/core/src/utils/json-utils.ts +33 -0
- package/node_modules/@posthog/core/src/utils/otlp-resource.spec.ts +55 -9
- package/node_modules/@posthog/core/src/utils/otlp-resource.ts +45 -4
- package/node_modules/@posthog/core/src/utils/retry-after.ts +135 -0
- package/node_modules/@posthog/types/dist/index.d.ts +1 -0
- package/node_modules/@posthog/types/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/types/dist/posthog-config.d.ts +10 -5
- package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
- package/node_modules/@posthog/types/dist/traces.d.ts +387 -0
- package/node_modules/@posthog/types/dist/traces.d.ts.map +1 -0
- package/node_modules/@posthog/types/dist/traces.js +18 -0
- package/node_modules/@posthog/types/dist/traces.mjs +1 -0
- package/node_modules/@posthog/types/package.json +1 -1
- package/node_modules/@posthog/types/src/index.ts +19 -0
- package/node_modules/@posthog/types/src/posthog-config.ts +10 -5
- package/node_modules/@posthog/types/src/traces.ts +429 -0
- package/node_modules/@zuplo/core/package.json +1 -1
- package/node_modules/@zuplo/graphql/package.json +1 -1
- package/node_modules/@zuplo/openapi-tools/package.json +1 -1
- package/node_modules/@zuplo/otel/package.json +1 -1
- package/node_modules/@zuplo/runtime/out/esm/{chunk-FEB2VFS7.js → chunk-KNAYKBEE.js} +96 -96
- package/node_modules/@zuplo/runtime/out/esm/chunk-KNAYKBEE.js.map +1 -0
- package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
- package/node_modules/@zuplo/runtime/package.json +1 -1
- package/package.json +11 -11
- package/node_modules/@zuplo/runtime/out/esm/chunk-FEB2VFS7.js.map +0 -1
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-FEB2VFS7.js.LEGAL.txt → chunk-KNAYKBEE.js.LEGAL.txt} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { LogAttributeValue } from '@posthog/types'
|
|
2
2
|
import { buildOtlpLogRecord, buildOtlpLogsPayload, buildResourceAttributes } from './logs-utils'
|
|
3
3
|
import { Logger, PostHogPersistedProperty } from '../types'
|
|
4
|
-
import { isArray, raceWithTimeout
|
|
4
|
+
import { isArray, raceWithTimeout } from '../utils'
|
|
5
|
+
import { FlushTimer } from '../utils/flush-timer'
|
|
6
|
+
import { RetryAfterWindow } from '../utils/retry-after'
|
|
7
|
+
import { MAX_FLUSH_BACKOFF_MS, NO_JITTER, backoffDelayMs, drawJitter } from '../utils/backoff'
|
|
5
8
|
import type { BufferedLogEntry, CaptureLogOptions, LogSdkContext, LogsHost, ResolvedPostHogLogsConfig } from './types'
|
|
6
9
|
|
|
7
10
|
// Caps the retry backoff at 2^6 = 64× the flush interval.
|
|
8
|
-
const MAX_FLUSH_BACKOFF_EXPONENT = 6
|
|
9
11
|
|
|
10
12
|
export class PostHogLogs {
|
|
11
13
|
private _maxBufferSize: number
|
|
@@ -17,7 +19,7 @@ export class PostHogLogs {
|
|
|
17
19
|
// one record after each successful send so a one-off oversized payload
|
|
18
20
|
// (e.g. a giant stack trace) doesn't permanently degrade throughput.
|
|
19
21
|
private _maxBatchRecordsPerPost: number
|
|
20
|
-
private _flushTimer
|
|
22
|
+
private readonly _flushTimer = new FlushTimer(() => this._flushInBackground())
|
|
21
23
|
// Serializes concurrent flushes — the second caller awaits the first rather
|
|
22
24
|
// than racing it and double-sending the same head-of-queue records.
|
|
23
25
|
private _flushPromise: Promise<void> | null = null
|
|
@@ -27,9 +29,12 @@ export class PostHogLogs {
|
|
|
27
29
|
// A batch captures this when it is assembled, so it can tell that the records it is
|
|
28
30
|
// holding no longer correspond to anything queued.
|
|
29
31
|
private _queueGeneration = 0
|
|
32
|
+
// Every path that can start a send checks this, not just the retry timer.
|
|
33
|
+
private _retryAfter = new RetryAfterWindow()
|
|
30
34
|
// Consecutive failed flushes; drives exponential backoff on the retry timer.
|
|
31
35
|
// A successful flush resets it to 0.
|
|
32
36
|
private _consecutiveFlushFailures = 0
|
|
37
|
+
private _flushJitter = NO_JITTER
|
|
33
38
|
|
|
34
39
|
// Fixed-window rate cap. Tumbling (not sliding) for cheap arithmetic on the
|
|
35
40
|
// hot path. Window rolls the first time `captureLog` fires after the window
|
|
@@ -83,7 +88,7 @@ export class PostHogLogs {
|
|
|
83
88
|
* and clears it separately (the browser empties its in-memory store).
|
|
84
89
|
*/
|
|
85
90
|
reset(): void {
|
|
86
|
-
this.
|
|
91
|
+
this._flushTimer.clear()
|
|
87
92
|
// `_flushPromise` is deliberately left alone: clearing it would let a second flush
|
|
88
93
|
// run alongside the in-flight one. Retiring that flush is `clearQueue`'s job,
|
|
89
94
|
// because the queue it holds belongs to the host, not to this state.
|
|
@@ -91,14 +96,28 @@ export class PostHogLogs {
|
|
|
91
96
|
this._intervalLogCount = 0
|
|
92
97
|
this._droppedWarned = false
|
|
93
98
|
this._consecutiveFlushFailures = 0
|
|
99
|
+
this._flushJitter = NO_JITTER
|
|
100
|
+
this._retryAfter.reset()
|
|
94
101
|
this._maxBatchRecordsPerPost = this._config.maxBatchRecordsPerPost
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
// Call when connectivity is restored: clear the failure backoff and flush now,
|
|
98
105
|
// so records don't wait out a (possibly minutes-long) backoff delay after the
|
|
99
106
|
// network returns. The host owns connectivity detection (web: `online` event).
|
|
107
|
+
// A `Retry-After` window survives this: the network coming back says nothing
|
|
108
|
+
// about the rate limit the endpoint set, and browsers fire `online` on every
|
|
109
|
+
// network handover.
|
|
100
110
|
onReconnect(): void {
|
|
101
111
|
this._consecutiveFlushFailures = 0
|
|
112
|
+
this._flushJitter = NO_JITTER
|
|
113
|
+
if (this._retryAfter.isOpen()) {
|
|
114
|
+
// The wait outlives the reconnect, but something still has to schedule
|
|
115
|
+
// the retry: an explicit `flush()` leaves no timer behind.
|
|
116
|
+
if (this._hasQueuedRecords()) {
|
|
117
|
+
this._armFlushTimer()
|
|
118
|
+
}
|
|
119
|
+
return
|
|
120
|
+
}
|
|
102
121
|
this._flushInBackground()
|
|
103
122
|
}
|
|
104
123
|
|
|
@@ -248,7 +267,7 @@ export class PostHogLogs {
|
|
|
248
267
|
}
|
|
249
268
|
|
|
250
269
|
private async _flushInner(): Promise<void> {
|
|
251
|
-
this.
|
|
270
|
+
this._flushTimer.clear()
|
|
252
271
|
|
|
253
272
|
let queue = this._instance.getPersistedProperty<BufferedLogEntry[]>(PostHogPersistedProperty.LogsQueue) ?? []
|
|
254
273
|
if (queue.length === 0) {
|
|
@@ -292,12 +311,22 @@ export class PostHogLogs {
|
|
|
292
311
|
if (outcome.kind === 'too-large' && batch.length > 1) {
|
|
293
312
|
this._maxBatchRecordsPerPost = Math.max(1, Math.floor(batch.length / 2))
|
|
294
313
|
this._logger.warn(
|
|
295
|
-
`
|
|
314
|
+
`Logs batch of size ${batch.length} was too large for the ingestion endpoint, reducing batch size to ${this._maxBatchRecordsPerPost}`
|
|
296
315
|
)
|
|
297
316
|
// Don't advance the queue — retry the same records with the smaller cap.
|
|
298
317
|
continue
|
|
299
318
|
}
|
|
300
319
|
|
|
320
|
+
// Not on the background wrapper: every lifecycle hook takes `flush()`,
|
|
321
|
+
// which does not go through it.
|
|
322
|
+
this._retryAfter.record(outcome)
|
|
323
|
+
|
|
324
|
+
// Outright, not through the ratchet: a timer a mid-flight capture armed
|
|
325
|
+
// is measured against a window this outcome may just have closed.
|
|
326
|
+
if (this._flushTimer.pending) {
|
|
327
|
+
this._flushTimer.arm(Math.max(this._flushIntervalMs, this._retryAfter.remainingMs()))
|
|
328
|
+
}
|
|
329
|
+
|
|
301
330
|
if (outcome.kind === 'retry-later') {
|
|
302
331
|
// Transient failure: keep records in the queue for the next flush cycle
|
|
303
332
|
// and surface the error so the caller can log/react.
|
|
@@ -305,13 +334,15 @@ export class PostHogLogs {
|
|
|
305
334
|
}
|
|
306
335
|
|
|
307
336
|
// ok | fatal | too-large-with-batch-of-1 → records are leaving the
|
|
308
|
-
// queue. 'fatal' and size-1
|
|
309
|
-
// same record forever. Surface the size-1
|
|
337
|
+
// queue. 'fatal' and size-1 refusals are dropped so we don't spin on the
|
|
338
|
+
// same record forever. Surface the size-1 refusal explicitly so a single
|
|
310
339
|
// oversized record (e.g. a giant body field) is visible in logs
|
|
311
340
|
// instead of silently disappearing.
|
|
312
341
|
if (outcome.kind === 'too-large') {
|
|
342
|
+
// Reached either from a 413 or from the size the SDK measured before
|
|
343
|
+
// sending, so the message names neither.
|
|
313
344
|
this._logger.warn(
|
|
314
|
-
'Dropping a single log record
|
|
345
|
+
'Dropping a single log record with batch size 1 — the record is larger than the server cap and cannot be split further.'
|
|
315
346
|
)
|
|
316
347
|
} else if (outcome.kind === 'ok' && this._maxBatchRecordsPerPost < this._config.maxBatchRecordsPerPost) {
|
|
317
348
|
// Linear recovery: each healthy send pushes the cap back up by 1
|
|
@@ -364,34 +395,40 @@ export class PostHogLogs {
|
|
|
364
395
|
this._instance.setPersistedProperty(PostHogPersistedProperty.LogsQueue, queue)
|
|
365
396
|
|
|
366
397
|
// Flush trigger: drain now rather than waiting for the timer. The queue may
|
|
367
|
-
// grow past this up to the eviction cap while the flush is in flight.
|
|
368
|
-
|
|
398
|
+
// grow past this up to the eviction cap while the flush is in flight. Not
|
|
399
|
+
// while the endpoint has asked us to wait: the size trigger is the dominant
|
|
400
|
+
// one on a busy host, so sending here would ignore the window entirely.
|
|
401
|
+
if (queue.length >= this._maxBufferSize && !this._retryAfter.isOpen()) {
|
|
369
402
|
this._flushInBackground()
|
|
370
403
|
return
|
|
371
404
|
}
|
|
372
405
|
|
|
373
|
-
// Arm one timer at a time; re-arming
|
|
406
|
+
// Arm one timer at a time; re-arming on every enqueue would push the flush out.
|
|
374
407
|
this._armFlushTimer()
|
|
375
408
|
}
|
|
376
409
|
|
|
377
410
|
// Arms the flush timer if none is pending. One-shot: the callback clears the
|
|
378
411
|
// handle so the next enqueue (or a flush that left records) schedules again.
|
|
379
|
-
private _armFlushTimer(
|
|
380
|
-
if (this._flushTimer) {
|
|
412
|
+
private _armFlushTimer(): void {
|
|
413
|
+
if (this._flushTimer.pending) {
|
|
381
414
|
return
|
|
382
415
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}, delayMs)
|
|
416
|
+
// Floored by any open window: `flush()` leaves no timer behind, so a
|
|
417
|
+
// capture after one arrives here.
|
|
418
|
+
this._flushTimer.arm(Math.max(this._flushIntervalMs, this._retryAfter.remainingMs()))
|
|
387
419
|
}
|
|
388
420
|
|
|
389
421
|
// Retry delay after a flush that left records: the first retry is at the base
|
|
390
|
-
// interval, then exponential backoff
|
|
391
|
-
//
|
|
422
|
+
// interval, then exponential backoff so a sustained outage isn't retried every
|
|
423
|
+
// interval. Jitter is drawn once per failure and reused, so two delays taken
|
|
424
|
+
// for the same failure cannot disagree.
|
|
392
425
|
private _nextFlushDelay(): number {
|
|
393
|
-
|
|
394
|
-
|
|
426
|
+
// A floor, not a replacement: the header never retries us sooner than our
|
|
427
|
+
// own backoff would have.
|
|
428
|
+
return Math.max(
|
|
429
|
+
backoffDelayMs(this._flushIntervalMs, this._consecutiveFlushFailures, this._flushJitter, MAX_FLUSH_BACKOFF_MS),
|
|
430
|
+
this._retryAfter.remainingMs()
|
|
431
|
+
)
|
|
395
432
|
}
|
|
396
433
|
|
|
397
434
|
private _hasQueuedRecords(): boolean {
|
|
@@ -410,7 +447,7 @@ export class PostHogLogs {
|
|
|
410
447
|
* fetchRetryDelay)`, which can exceed the caller's shutdown SLA.
|
|
411
448
|
*/
|
|
412
449
|
async shutdown(timeoutMs?: number): Promise<void> {
|
|
413
|
-
this.
|
|
450
|
+
this._flushTimer.clear()
|
|
414
451
|
const flushPromise = this.flush().catch(() => {
|
|
415
452
|
// Best-effort: a logs-flush failure during shutdown is not actionable
|
|
416
453
|
// and must not prevent the rest of shutdown from running. Errors are
|
|
@@ -449,9 +486,11 @@ export class PostHogLogs {
|
|
|
449
486
|
.then(
|
|
450
487
|
() => {
|
|
451
488
|
this._consecutiveFlushFailures = 0
|
|
489
|
+
this._flushJitter = NO_JITTER
|
|
452
490
|
},
|
|
453
491
|
(err) => {
|
|
454
492
|
this._consecutiveFlushFailures++
|
|
493
|
+
this._flushJitter = drawJitter()
|
|
455
494
|
this._logger.error('PostHog logs flush failed:', err)
|
|
456
495
|
}
|
|
457
496
|
)
|
|
@@ -460,15 +499,8 @@ export class PostHogLogs {
|
|
|
460
499
|
// sit undelivered on a quiet page; re-arm so the timer retries them, backing
|
|
461
500
|
// off on consecutive failures.
|
|
462
501
|
if (!this._instance.isDisabled && this._hasQueuedRecords()) {
|
|
463
|
-
this.
|
|
502
|
+
this._flushTimer.armNoEarlierThan(this._nextFlushDelay())
|
|
464
503
|
}
|
|
465
504
|
})
|
|
466
505
|
}
|
|
467
|
-
|
|
468
|
-
private _clearFlushTimer(): void {
|
|
469
|
-
if (this._flushTimer) {
|
|
470
|
-
clearTimeout(this._flushTimer)
|
|
471
|
-
this._flushTimer = undefined
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
506
|
}
|
|
@@ -12,7 +12,7 @@ import type { LogSdkContext, ResolvedPostHogLogsConfig } from './types'
|
|
|
12
12
|
import { isNullish, isNumber, isUndefined } from '../utils'
|
|
13
13
|
import { sanitizeString, UNSERIALIZABLE_VALUE } from '../utils/json-utils'
|
|
14
14
|
import { toOtlpKeyValueList } from '../utils/otlp-any-value'
|
|
15
|
-
import { buildOtlpResourceAttributes } from '../utils/otlp-resource'
|
|
15
|
+
import { buildOtlpResourceAttributes, toOtlpResourceKeyValueList } from '../utils/otlp-resource'
|
|
16
16
|
|
|
17
17
|
// ============================================================================
|
|
18
18
|
// Severity mapping
|
|
@@ -206,7 +206,7 @@ export function buildOtlpLogsPayload(
|
|
|
206
206
|
return {
|
|
207
207
|
resourceLogs: [
|
|
208
208
|
{
|
|
209
|
-
resource: { attributes:
|
|
209
|
+
resource: { attributes: toOtlpResourceKeyValueList(resourceAttributes) },
|
|
210
210
|
scopeLogs: [
|
|
211
211
|
{
|
|
212
212
|
scope: { name: scopeName, version: scopeVersion },
|
|
@@ -128,8 +128,8 @@ export interface PostHogLogsConfig {
|
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Max records per outbound POST. Keeps each request under the server's
|
|
131
|
-
*
|
|
132
|
-
* same records, then ramps back up by 1 per healthy send. A 413 on a
|
|
131
|
+
* request body cap. On a 413 response, the SDK halves this value, retries
|
|
132
|
+
* the same records, then ramps back up by 1 per healthy send. A 413 on a
|
|
133
133
|
* single-record batch drops the record (it's larger than the server can
|
|
134
134
|
* accept regardless of batch size). Default: 50 (RN) / 100 (browser).
|
|
135
135
|
*/
|
|
@@ -50,6 +50,9 @@ describe('PostHogMetrics', () => {
|
|
|
50
50
|
vi.useFakeTimers()
|
|
51
51
|
mockInstance = createMockInstance()
|
|
52
52
|
logger = createMockLogger()
|
|
53
|
+
// Retry delays carry jitter; pinned to its midpoint so every timing
|
|
54
|
+
// assertion here measures the backoff itself and cannot flake.
|
|
55
|
+
vi.spyOn(Math, 'random').mockReturnValue(0.5)
|
|
53
56
|
})
|
|
54
57
|
|
|
55
58
|
afterEach(() => {
|
|
@@ -243,6 +246,263 @@ describe('PostHogMetrics', () => {
|
|
|
243
246
|
expect(mockInstance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
244
247
|
})
|
|
245
248
|
|
|
249
|
+
it('keeps flushing on the interval while captures keep arriving', async () => {
|
|
250
|
+
// Every capture arms the timer, and metrics have no size trigger to fall
|
|
251
|
+
// back on: re-arming a pending one would stop the window ever being sent.
|
|
252
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 })
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < 60; i++) {
|
|
255
|
+
metrics.count('orders_created', 1)
|
|
256
|
+
await vi.advanceTimersByTimeAsync(1000)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
expect(mockInstance._sendMetricsBatch).toHaveBeenCalled()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('waits out Retry-After even when a flush timer is already pending', async () => {
|
|
263
|
+
// The capture arms a timer at the flush interval; the failed flush then asks
|
|
264
|
+
// for far longer. The pending timer must not fire first.
|
|
265
|
+
const instance = createMockInstance({
|
|
266
|
+
_sendMetricsBatch: vi.fn((): Promise<SendMetricsBatchOutcome> =>
|
|
267
|
+
Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
|
|
268
|
+
),
|
|
269
|
+
})
|
|
270
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 }, instance)
|
|
271
|
+
metrics.count('orders_created', 1)
|
|
272
|
+
await metrics.flush()
|
|
273
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
274
|
+
|
|
275
|
+
await vi.advanceTimersByTimeAsync(11_000)
|
|
276
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
277
|
+
|
|
278
|
+
await vi.advanceTimersByTimeAsync(300_000)
|
|
279
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('clears Retry-After on an outcome that is not a retry', async () => {
|
|
283
|
+
// Only a retriable outcome carries a wait. A stale one left set here would
|
|
284
|
+
// pin every later flush at the server's old window forever.
|
|
285
|
+
const outcomes: SendMetricsBatchOutcome[] = [
|
|
286
|
+
{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 },
|
|
287
|
+
{ kind: 'fatal', error: new Error('400') },
|
|
288
|
+
]
|
|
289
|
+
const instance = createMockInstance({
|
|
290
|
+
_sendMetricsBatch: vi.fn(() => Promise.resolve(outcomes.shift() ?? { kind: 'ok' })),
|
|
291
|
+
})
|
|
292
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 }, instance)
|
|
293
|
+
metrics.count('orders_created', 1)
|
|
294
|
+
await metrics.flush()
|
|
295
|
+
|
|
296
|
+
// The wait elapses, the retry lands a 400, and that ends the wait.
|
|
297
|
+
await vi.advanceTimersByTimeAsync(300_000)
|
|
298
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
299
|
+
|
|
300
|
+
metrics.count('orders_created', 1)
|
|
301
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
302
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(3)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('keeps the Retry-After window when a batch is refused for size', async () => {
|
|
306
|
+
// `too-large` is a verdict on the body's size — the SDK's own or a 413 —
|
|
307
|
+
// so it says nothing about the endpoint's rate limit. Ending the wait on
|
|
308
|
+
// it lets the next refusal install a fresh window, pushing the retry out
|
|
309
|
+
// past the deadline the endpoint actually named — observed here as the
|
|
310
|
+
// flush landing at 300s rather than at 310s.
|
|
311
|
+
const outcomes: SendMetricsBatchOutcome[] = [
|
|
312
|
+
{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 },
|
|
313
|
+
{ kind: 'too-large' },
|
|
314
|
+
{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 },
|
|
315
|
+
]
|
|
316
|
+
const instance = createMockInstance({
|
|
317
|
+
_sendMetricsBatch: vi.fn(() => Promise.resolve(outcomes.shift() ?? { kind: 'ok' })),
|
|
318
|
+
})
|
|
319
|
+
const metrics = createMetrics({ flushIntervalMs: 1000 }, instance)
|
|
320
|
+
metrics.count('orders_created', 1)
|
|
321
|
+
await metrics.flush()
|
|
322
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
323
|
+
|
|
324
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
325
|
+
await metrics.flush()
|
|
326
|
+
metrics.count('orders_created', 1)
|
|
327
|
+
await metrics.flush()
|
|
328
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(3)
|
|
329
|
+
|
|
330
|
+
await vi.advanceTimersByTimeAsync(295_000)
|
|
331
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(4)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('closes the window at the ceiling for a host out-pacing it', async () => {
|
|
335
|
+
// Each refusal sliding the deadline would keep `_nextFlushDelay` pinned at
|
|
336
|
+
// the full window, so the flush cadence would never recover.
|
|
337
|
+
const instance = createMockInstance({
|
|
338
|
+
_sendMetricsBatch: vi.fn((): Promise<SendMetricsBatchOutcome> =>
|
|
339
|
+
Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 30_000 })
|
|
340
|
+
),
|
|
341
|
+
})
|
|
342
|
+
const metrics = createMetrics({ flushIntervalMs: 60_000 }, instance)
|
|
343
|
+
metrics.count('orders_created', 1)
|
|
344
|
+
await metrics.flush()
|
|
345
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
346
|
+
|
|
347
|
+
// Sampled: whether a given moment falls inside a window is timing
|
|
348
|
+
// dependent, but it must fall outside one sometimes.
|
|
349
|
+
let sawWindowClosed = false
|
|
350
|
+
for (let i = 0; i < 70; i++) {
|
|
351
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
352
|
+
// Sampled before the flush: a flush that finds the window closed opens
|
|
353
|
+
// a fresh one, so sampling after it would always look open.
|
|
354
|
+
if ((metrics as any)._retryAfter.remainingMs() === 0) {
|
|
355
|
+
sawWindowClosed = true
|
|
356
|
+
}
|
|
357
|
+
metrics.count('orders_created', 1)
|
|
358
|
+
await metrics.flush()
|
|
359
|
+
}
|
|
360
|
+
expect(sawWindowClosed).toBe(true)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('does not install a Retry-After that lands after reset', async () => {
|
|
364
|
+
let settle: ((outcome: SendMetricsBatchOutcome) => void) | undefined
|
|
365
|
+
const instance = createMockInstance({
|
|
366
|
+
_sendMetricsBatch: vi.fn(
|
|
367
|
+
(): Promise<SendMetricsBatchOutcome> =>
|
|
368
|
+
new Promise((resolve) => {
|
|
369
|
+
settle = resolve
|
|
370
|
+
})
|
|
371
|
+
),
|
|
372
|
+
})
|
|
373
|
+
const metrics = createMetrics({ flushIntervalMs: 1000 }, instance)
|
|
374
|
+
metrics.count('orders_created', 1)
|
|
375
|
+
await vi.advanceTimersByTimeAsync(1000)
|
|
376
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
377
|
+
|
|
378
|
+
metrics.reset()
|
|
379
|
+
|
|
380
|
+
// Settle first: the capture that arms the next timer must not find a
|
|
381
|
+
// window belonging to the client that was just torn down.
|
|
382
|
+
settle?.({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
|
|
383
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
384
|
+
|
|
385
|
+
metrics.count('orders_created', 1)
|
|
386
|
+
await vi.advanceTimersByTimeAsync(1000)
|
|
387
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('drops a Retry-After wait on reset', async () => {
|
|
391
|
+
const instance = createMockInstance({
|
|
392
|
+
_sendMetricsBatch: vi.fn((): Promise<SendMetricsBatchOutcome> =>
|
|
393
|
+
Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
|
|
394
|
+
),
|
|
395
|
+
})
|
|
396
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 }, instance)
|
|
397
|
+
metrics.count('orders_created', 1)
|
|
398
|
+
await metrics.flush()
|
|
399
|
+
|
|
400
|
+
metrics.reset()
|
|
401
|
+
metrics.count('orders_created', 1)
|
|
402
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
403
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('does not restart a served-out wait for a sample captured mid-retry', async () => {
|
|
407
|
+
// A deadline, not a duration. The retry's timer has already fired, so the
|
|
408
|
+
// capture below is the one that arms the next timer — and it sees the
|
|
409
|
+
// wait still set, because the send it belongs to has not settled. Holding
|
|
410
|
+
// a duration here re-arms for the whole window a second time and leaves
|
|
411
|
+
// the sample 300s behind on an endpoint that has already recovered.
|
|
412
|
+
let settleRetry: ((outcome: SendMetricsBatchOutcome) => void) | undefined
|
|
413
|
+
let call = 0
|
|
414
|
+
const instance = createMockInstance({
|
|
415
|
+
_sendMetricsBatch: vi.fn((): Promise<SendMetricsBatchOutcome> => {
|
|
416
|
+
call++
|
|
417
|
+
if (call === 1) {
|
|
418
|
+
return Promise.resolve({ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 })
|
|
419
|
+
}
|
|
420
|
+
return new Promise<SendMetricsBatchOutcome>((resolve) => {
|
|
421
|
+
settleRetry = resolve
|
|
422
|
+
})
|
|
423
|
+
}),
|
|
424
|
+
})
|
|
425
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 }, instance)
|
|
426
|
+
metrics.count('orders_created', 1)
|
|
427
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
428
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
429
|
+
|
|
430
|
+
// The wait elapses and the retry goes out, but hangs.
|
|
431
|
+
await vi.advanceTimersByTimeAsync(300_000)
|
|
432
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
433
|
+
|
|
434
|
+
metrics.count('orders_created', 1)
|
|
435
|
+
settleRetry?.({ kind: 'ok' })
|
|
436
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
437
|
+
|
|
438
|
+
// One interval, not another window.
|
|
439
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
440
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(3)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('keeps its own interval when the endpoint asks for less', async () => {
|
|
444
|
+
const instance = createMockInstance({
|
|
445
|
+
_sendMetricsBatch: vi.fn((): Promise<SendMetricsBatchOutcome> =>
|
|
446
|
+
Promise.resolve({ kind: 'retry-later', error: new Error('503'), retryAfterMs: 10 })
|
|
447
|
+
),
|
|
448
|
+
})
|
|
449
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 }, instance)
|
|
450
|
+
metrics.count('orders_created', 1)
|
|
451
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
452
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
453
|
+
|
|
454
|
+
await vi.advanceTimersByTimeAsync(1000)
|
|
455
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
456
|
+
|
|
457
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
458
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('sends on an explicit flush inside the window', async () => {
|
|
462
|
+
// Lifecycle drains (RN background, shutdown) must not become no-ops for
|
|
463
|
+
// the length of a window; metrics has no `force` escape hatch.
|
|
464
|
+
const outcomes: SendMetricsBatchOutcome[] = [
|
|
465
|
+
{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 },
|
|
466
|
+
]
|
|
467
|
+
const instance = createMockInstance({
|
|
468
|
+
_sendMetricsBatch: vi.fn((): Promise<SendMetricsBatchOutcome> =>
|
|
469
|
+
Promise.resolve(outcomes.shift() ?? { kind: 'ok' })
|
|
470
|
+
),
|
|
471
|
+
})
|
|
472
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 }, instance)
|
|
473
|
+
metrics.count('orders_created', 1)
|
|
474
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
475
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
476
|
+
|
|
477
|
+
metrics.count('orders_created', 1)
|
|
478
|
+
await metrics.flush()
|
|
479
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('ends the wait when a later failure names none', async () => {
|
|
483
|
+
const outcomes: SendMetricsBatchOutcome[] = [
|
|
484
|
+
{ kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 },
|
|
485
|
+
{ kind: 'retry-later', error: new Error('503') },
|
|
486
|
+
]
|
|
487
|
+
const instance = createMockInstance({
|
|
488
|
+
_sendMetricsBatch: vi.fn((): Promise<SendMetricsBatchOutcome> =>
|
|
489
|
+
Promise.resolve(outcomes.shift() ?? { kind: 'ok' })
|
|
490
|
+
),
|
|
491
|
+
})
|
|
492
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 }, instance)
|
|
493
|
+
metrics.count('orders_created', 1)
|
|
494
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
495
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
496
|
+
|
|
497
|
+
await vi.advanceTimersByTimeAsync(300_000)
|
|
498
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
499
|
+
|
|
500
|
+
// Off the 300s window and back on our own backoff: two consecutive
|
|
501
|
+
// failures, so one doubling of the interval rather than another 300s.
|
|
502
|
+
await vi.advanceTimersByTimeAsync(20_000)
|
|
503
|
+
expect(instance._sendMetricsBatch).toHaveBeenCalledTimes(3)
|
|
504
|
+
})
|
|
505
|
+
|
|
246
506
|
it('does not send when the window is empty', async () => {
|
|
247
507
|
createMetrics({ flushIntervalMs: 5000 })
|
|
248
508
|
await vi.advanceTimersByTimeAsync(15000)
|
|
@@ -519,4 +779,48 @@ describe('PostHogMetrics', () => {
|
|
|
519
779
|
expect(optedOutInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
520
780
|
})
|
|
521
781
|
})
|
|
782
|
+
|
|
783
|
+
it('does not hold a later capture at a window a successful flush already closed', async () => {
|
|
784
|
+
let sends = 0
|
|
785
|
+
mockInstance._sendMetricsBatch = vi.fn(async (): Promise<SendMetricsBatchOutcome> => {
|
|
786
|
+
sends += 1
|
|
787
|
+
return sends === 1 ? { kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 } : { kind: 'ok' }
|
|
788
|
+
})
|
|
789
|
+
const metrics = createMetrics({ flushIntervalMs: 5000 })
|
|
790
|
+
|
|
791
|
+
metrics.count('a', 1)
|
|
792
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
793
|
+
await metrics.flush()
|
|
794
|
+
const closedAt = Date.now()
|
|
795
|
+
|
|
796
|
+
metrics.count('b', 1)
|
|
797
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
798
|
+
|
|
799
|
+
expect(mockInstance._sendMetricsBatch).toHaveBeenCalledTimes(3)
|
|
800
|
+
expect(Date.now() - closedAt).toBeLessThanOrEqual(5000)
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
it('releases a series captured mid-flush once that flush closes the window', async () => {
|
|
804
|
+
const metrics = createMetrics({ flushIntervalMs: 10_000 })
|
|
805
|
+
let sends = 0
|
|
806
|
+
mockInstance._sendMetricsBatch = vi.fn(async (): Promise<SendMetricsBatchOutcome> => {
|
|
807
|
+
sends += 1
|
|
808
|
+
if (sends === 1) {
|
|
809
|
+
return { kind: 'retry-later', error: new Error('429'), retryAfterMs: 300_000 }
|
|
810
|
+
}
|
|
811
|
+
await Promise.resolve()
|
|
812
|
+
if (sends === 2) {
|
|
813
|
+
metrics.count('mid', 1)
|
|
814
|
+
}
|
|
815
|
+
return { kind: 'ok' }
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
metrics.count('a', 1)
|
|
819
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
820
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
821
|
+
await metrics.flush()
|
|
822
|
+
|
|
823
|
+
await vi.advanceTimersByTimeAsync(10_000)
|
|
824
|
+
expect(mockInstance._sendMetricsBatch).toHaveBeenCalledTimes(3)
|
|
825
|
+
})
|
|
522
826
|
})
|