@zuplo/cli 6.72.0 → 6.72.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.
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify/.github/dependabot.yml +42 -2
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify/README.md +5 -5
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify/index.js +4 -7
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify/package.json +3 -3
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify/test/additionalProperties.test.js +47 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify/test/const.test.js +20 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify/test/patternProperties.test.js +24 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/.gitattributes +2 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/.github/dependabot.yml +53 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/.github/workflows/ci.yml +154 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/.github/workflows/lock-threads.yml +19 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/LICENSE +30 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/README.md +152 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/benchmark/benchmark.mjs +159 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/benchmark/encoder.mjs +54 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/benchmark/equal.mjs +51 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/benchmark/non-simple-domain.mjs +22 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/benchmark/package.json +17 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/benchmark/string-array-to-hex-stripped.mjs +24 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/benchmark/ws-is-secure.mjs +65 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/eslint.config.js +6 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/index.js +405 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/lib/schemes.js +267 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/lib/utils.js +585 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/package.json +68 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/ajv.test.js +43 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/equal.test.js +168 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/fixtures/uri-js-parse.json +501 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/fixtures/uri-js-serialize.json +120 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/parse.test.js +325 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/resolve.test.js +87 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/rfc-3986.test.js +90 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/scheme-normalization.test.js +16 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/security-normalization.test.js +39 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/security-utf8-encoding.test.js +0 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/security.test.js +161 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/serialize.test.js +151 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/uri-js-compatibility.test.js +33 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/uri-js.test.js +912 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/test/util.test.js +47 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/tsconfig.json +10 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/types/index.d.ts +51 -0
- package/node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-uri/types/index.tst.ts +15 -0
- package/node_modules/@posthog/core/dist/index.d.ts +3 -0
- package/node_modules/@posthog/core/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/index.js +133 -93
- package/node_modules/@posthog/core/dist/index.mjs +2 -1
- package/node_modules/@posthog/core/dist/metrics/index.d.ts +61 -0
- package/node_modules/@posthog/core/dist/metrics/index.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/metrics/index.js +326 -0
- package/node_modules/@posthog/core/dist/metrics/index.mjs +283 -0
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.d.ts +41 -0
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.js +117 -0
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.mjs +68 -0
- package/node_modules/@posthog/core/dist/metrics/types.d.ts +69 -0
- package/node_modules/@posthog/core/dist/metrics/types.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/metrics/types.js +18 -0
- package/node_modules/@posthog/core/dist/metrics/types.mjs +0 -0
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts +2 -0
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.js +16 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +16 -1
- package/node_modules/@posthog/core/package.json +2 -2
- package/node_modules/@posthog/core/src/index.ts +20 -0
- package/node_modules/@posthog/core/src/metrics/index.spec.ts +459 -0
- package/node_modules/@posthog/core/src/metrics/index.ts +414 -0
- package/node_modules/@posthog/core/src/metrics/metrics-utils.ts +100 -0
- package/node_modules/@posthog/core/src/metrics/types.ts +89 -0
- package/node_modules/@posthog/core/src/posthog-core-stateless.ts +26 -0
- package/node_modules/@posthog/types/dist/capture-metric.d.ts +122 -0
- package/node_modules/@posthog/types/dist/capture-metric.d.ts.map +1 -0
- package/node_modules/@posthog/types/dist/capture-metric.js +36 -0
- package/node_modules/@posthog/types/dist/capture-metric.mjs +2 -0
- package/node_modules/@posthog/types/dist/index.d.ts +3 -1
- package/node_modules/@posthog/types/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/types/dist/index.js +19 -1
- package/node_modules/@posthog/types/dist/index.mjs +2 -0
- package/node_modules/@posthog/types/dist/posthog-config.d.ts +60 -0
- package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
- package/node_modules/@posthog/types/package.json +1 -1
- package/node_modules/@posthog/types/src/capture-metric.ts +133 -0
- package/node_modules/@posthog/types/src/index.ts +17 -0
- package/node_modules/@posthog/types/src/posthog-config.ts +62 -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-FAOWNDMK.js → chunk-IC7XGD5C.js} +93 -93
- package/node_modules/@zuplo/runtime/out/esm/chunk-IC7XGD5C.js.map +1 -0
- package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/index.js.map +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/types/index.d.ts +830 -6
- package/node_modules/@zuplo/runtime/package.json +1 -1
- package/package.json +6 -6
- package/node_modules/@zuplo/runtime/out/esm/chunk-FAOWNDMK.js.map +0 -1
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-FAOWNDMK.js.LEGAL.txt → chunk-IC7XGD5C.js.LEGAL.txt} +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import type { Logger } from '../types'
|
|
2
|
+
import { PostHogMetrics } from './index'
|
|
3
|
+
import type { OtlpMetric, OtlpMetricsPayload, ResolvedPostHogMetricsConfig, SendMetricsBatchOutcome } from './types'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 10000
|
|
6
|
+
const DEFAULT_MAX_SERIES_PER_FLUSH = 1000
|
|
7
|
+
|
|
8
|
+
const resolveForTest = (partial?: Partial<ResolvedPostHogMetricsConfig>): ResolvedPostHogMetricsConfig => ({
|
|
9
|
+
...partial,
|
|
10
|
+
flushIntervalMs: partial?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
11
|
+
maxSeriesPerFlush: partial?.maxSeriesPerFlush ?? DEFAULT_MAX_SERIES_PER_FLUSH,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const createMockInstance = (overrides: Record<string, any> = {}): any => ({
|
|
15
|
+
isDisabled: false,
|
|
16
|
+
optedOut: false,
|
|
17
|
+
getLibraryId: jest.fn(() => 'posthog-core-tests'),
|
|
18
|
+
getLibraryVersion: jest.fn(() => '0.0.0-test'),
|
|
19
|
+
_sendMetricsBatch: jest.fn((): Promise<SendMetricsBatchOutcome> => Promise.resolve({ kind: 'ok' })),
|
|
20
|
+
...overrides,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const createMockLogger = (): Logger => {
|
|
24
|
+
const logger: any = {
|
|
25
|
+
debug: jest.fn(),
|
|
26
|
+
info: jest.fn(),
|
|
27
|
+
warn: jest.fn(),
|
|
28
|
+
error: jest.fn(),
|
|
29
|
+
critical: jest.fn(),
|
|
30
|
+
}
|
|
31
|
+
logger.createLogger = jest.fn(() => logger)
|
|
32
|
+
return logger as Logger
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('PostHogMetrics', () => {
|
|
36
|
+
let mockInstance: any
|
|
37
|
+
let logger: Logger
|
|
38
|
+
|
|
39
|
+
const createMetrics = (config?: Partial<ResolvedPostHogMetricsConfig>, instance?: any): PostHogMetrics => {
|
|
40
|
+
return new PostHogMetrics(instance ?? mockInstance, resolveForTest(config), logger)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sentPayloads = (instance?: any): OtlpMetricsPayload[] =>
|
|
44
|
+
(instance ?? mockInstance)._sendMetricsBatch.mock.calls.map((c: any[]) => c[0])
|
|
45
|
+
|
|
46
|
+
const sentMetrics = (instance?: any): OtlpMetric[] =>
|
|
47
|
+
sentPayloads(instance).flatMap((p) => p.resourceMetrics[0].scopeMetrics[0].metrics)
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
jest.useFakeTimers()
|
|
51
|
+
mockInstance = createMockInstance()
|
|
52
|
+
logger = createMockLogger()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.useRealTimers()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('aggregation semantics', () => {
|
|
60
|
+
it('sums count samples for the same series into one delta data point', async () => {
|
|
61
|
+
const metrics = createMetrics()
|
|
62
|
+
metrics.count('orders_created', 1)
|
|
63
|
+
metrics.count('orders_created', 1)
|
|
64
|
+
metrics.count('orders_created', 3)
|
|
65
|
+
await metrics.flush()
|
|
66
|
+
|
|
67
|
+
const sent = sentMetrics()
|
|
68
|
+
expect(sent).toHaveLength(1)
|
|
69
|
+
expect(sent[0].name).toBe('orders_created')
|
|
70
|
+
expect(sent[0].sum).toMatchObject({ aggregationTemporality: 1, isMonotonic: true })
|
|
71
|
+
expect(sent[0].sum!.dataPoints).toHaveLength(1)
|
|
72
|
+
expect(sent[0].sum!.dataPoints[0].asDouble).toBe(5)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('defaults count value to 1', async () => {
|
|
76
|
+
const metrics = createMetrics()
|
|
77
|
+
metrics.count('clicks')
|
|
78
|
+
metrics.count('clicks')
|
|
79
|
+
await metrics.flush()
|
|
80
|
+
|
|
81
|
+
expect(sentMetrics()[0].sum!.dataPoints[0].asDouble).toBe(2)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('keeps only the last gauge value per series per window', async () => {
|
|
85
|
+
const metrics = createMetrics()
|
|
86
|
+
metrics.gauge('active_connections', 42)
|
|
87
|
+
metrics.gauge('active_connections', 17)
|
|
88
|
+
await metrics.flush()
|
|
89
|
+
|
|
90
|
+
const sent = sentMetrics()
|
|
91
|
+
expect(sent).toHaveLength(1)
|
|
92
|
+
expect(sent[0].gauge!.dataPoints).toHaveLength(1)
|
|
93
|
+
expect(sent[0].gauge!.dataPoints[0].asDouble).toBe(17)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('accumulates histogram observations into count/sum/min/max and buckets', async () => {
|
|
97
|
+
const metrics = createMetrics()
|
|
98
|
+
metrics.histogram('api_latency', 10, { unit: 'ms' })
|
|
99
|
+
metrics.histogram('api_latency', 20, { unit: 'ms' })
|
|
100
|
+
metrics.histogram('api_latency', 30, { unit: 'ms' })
|
|
101
|
+
await metrics.flush()
|
|
102
|
+
|
|
103
|
+
const sent = sentMetrics()
|
|
104
|
+
expect(sent).toHaveLength(1)
|
|
105
|
+
expect(sent[0].unit).toBe('ms')
|
|
106
|
+
const dp = sent[0].histogram!.dataPoints[0]
|
|
107
|
+
expect(dp.count).toBe(3)
|
|
108
|
+
expect(dp.sum).toBe(60)
|
|
109
|
+
expect(dp.min).toBe(10)
|
|
110
|
+
expect(dp.max).toBe(30)
|
|
111
|
+
expect(dp.explicitBounds.length).toBeGreaterThan(0)
|
|
112
|
+
expect(dp.bucketCounts).toHaveLength(dp.explicitBounds.length + 1)
|
|
113
|
+
expect(dp.bucketCounts.reduce((a, b) => a + b, 0)).toBe(3)
|
|
114
|
+
expect(sent[0].histogram!.aggregationTemporality).toBe(1)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('places histogram observations in the correct buckets', async () => {
|
|
118
|
+
const metrics = createMetrics()
|
|
119
|
+
// Default OTel bounds start [0, 5, 10, 25, ...]: -1 → bucket 0 (≤0),
|
|
120
|
+
// 3 → bucket 1 (≤5), 10 → bucket 2 (≤10), 1e9 → overflow bucket.
|
|
121
|
+
metrics.histogram('x', -1)
|
|
122
|
+
metrics.histogram('x', 3)
|
|
123
|
+
metrics.histogram('x', 10)
|
|
124
|
+
metrics.histogram('x', 1e9)
|
|
125
|
+
await metrics.flush()
|
|
126
|
+
|
|
127
|
+
const dp = sentMetrics()[0].histogram!.dataPoints[0]
|
|
128
|
+
expect(dp.bucketCounts[0]).toBe(1)
|
|
129
|
+
expect(dp.bucketCounts[1]).toBe(1)
|
|
130
|
+
expect(dp.bucketCounts[2]).toBe(1)
|
|
131
|
+
expect(dp.bucketCounts[dp.bucketCounts.length - 1]).toBe(1)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('splits series by attribute values but merges identical attribute sets regardless of key order', async () => {
|
|
135
|
+
const metrics = createMetrics()
|
|
136
|
+
metrics.count('api_calls', 1, { attributes: { route: '/home', plan: 'free' } })
|
|
137
|
+
metrics.count('api_calls', 1, { attributes: { plan: 'free', route: '/home' } })
|
|
138
|
+
metrics.count('api_calls', 1, { attributes: { route: '/checkout', plan: 'free' } })
|
|
139
|
+
await metrics.flush()
|
|
140
|
+
|
|
141
|
+
const sent = sentMetrics()
|
|
142
|
+
expect(sent).toHaveLength(1)
|
|
143
|
+
expect(sent[0].sum!.dataPoints).toHaveLength(2)
|
|
144
|
+
const values = sent[0].sum!.dataPoints.map((dp) => dp.asDouble).sort()
|
|
145
|
+
expect(values).toEqual([1, 2])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('emits separate metric entries for the same name with different types or units', async () => {
|
|
149
|
+
const metrics = createMetrics()
|
|
150
|
+
metrics.count('throughput', 1)
|
|
151
|
+
metrics.gauge('throughput', 5)
|
|
152
|
+
metrics.histogram('size', 1, { unit: 'byte' })
|
|
153
|
+
metrics.histogram('size', 1, { unit: 'kilobyte' })
|
|
154
|
+
await metrics.flush()
|
|
155
|
+
|
|
156
|
+
const sent = sentMetrics()
|
|
157
|
+
expect(sent).toHaveLength(4)
|
|
158
|
+
const throughput = sent.filter((m) => m.name === 'throughput')
|
|
159
|
+
expect(throughput.some((m) => m.sum)).toBe(true)
|
|
160
|
+
expect(throughput.some((m) => m.gauge)).toBe(true)
|
|
161
|
+
const sizes = sent.filter((m) => m.name === 'size')
|
|
162
|
+
expect(sizes.map((m) => m.unit).sort()).toEqual(['byte', 'kilobyte'])
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('encodes attributes as OTLP key-value pairs on the data point', async () => {
|
|
166
|
+
const metrics = createMetrics()
|
|
167
|
+
metrics.count('api_calls', 1, { attributes: { route: '/home', retries: 2, cached: true } })
|
|
168
|
+
await metrics.flush()
|
|
169
|
+
|
|
170
|
+
const attrs = sentMetrics()[0].sum!.dataPoints[0].attributes
|
|
171
|
+
expect(attrs).toContainEqual({ key: 'route', value: { stringValue: '/home' } })
|
|
172
|
+
expect(attrs).toContainEqual({ key: 'retries', value: { intValue: 2 } })
|
|
173
|
+
expect(attrs).toContainEqual({ key: 'cached', value: { boolValue: true } })
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('stamps delta data points with a window: startTimeUnixNano <= timeUnixNano, both nano strings', async () => {
|
|
177
|
+
const metrics = createMetrics()
|
|
178
|
+
metrics.count('orders_created', 1)
|
|
179
|
+
jest.advanceTimersByTime(2000)
|
|
180
|
+
await metrics.flush()
|
|
181
|
+
|
|
182
|
+
const dp = sentMetrics()[0].sum!.dataPoints[0]
|
|
183
|
+
expect(dp.timeUnixNano).toMatch(/^\d+$/)
|
|
184
|
+
expect(dp.startTimeUnixNano).toMatch(/^\d+$/)
|
|
185
|
+
expect(BigInt(dp.startTimeUnixNano!)).toBeLessThanOrEqual(BigInt(dp.timeUnixNano))
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('resource attributes', () => {
|
|
190
|
+
it('attaches service.name, sdk identity, and user resource attributes', async () => {
|
|
191
|
+
const metrics = createMetrics({
|
|
192
|
+
serviceName: 'checkout-web',
|
|
193
|
+
environment: 'production',
|
|
194
|
+
resourceAttributes: { 'k8s.pod': 'web-1' },
|
|
195
|
+
})
|
|
196
|
+
metrics.count('orders_created', 1)
|
|
197
|
+
await metrics.flush()
|
|
198
|
+
|
|
199
|
+
const payload = sentPayloads()[0]
|
|
200
|
+
const resourceAttrs = payload.resourceMetrics[0].resource.attributes
|
|
201
|
+
expect(resourceAttrs).toContainEqual({ key: 'service.name', value: { stringValue: 'checkout-web' } })
|
|
202
|
+
expect(resourceAttrs).toContainEqual({ key: 'deployment.environment', value: { stringValue: 'production' } })
|
|
203
|
+
expect(resourceAttrs).toContainEqual({ key: 'k8s.pod', value: { stringValue: 'web-1' } })
|
|
204
|
+
expect(resourceAttrs).toContainEqual({ key: 'telemetry.sdk.name', value: { stringValue: 'posthog-core-tests' } })
|
|
205
|
+
expect(payload.resourceMetrics[0].scopeMetrics[0].scope).toEqual({
|
|
206
|
+
name: 'posthog-core-tests',
|
|
207
|
+
version: '0.0.0-test',
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('falls back to unknown_service and never lets resourceAttributes clobber service.name', async () => {
|
|
212
|
+
const metrics = createMetrics({ resourceAttributes: { 'service.name': 'spoofed' } })
|
|
213
|
+
metrics.count('x', 1)
|
|
214
|
+
await metrics.flush()
|
|
215
|
+
|
|
216
|
+
const resourceAttrs = sentPayloads()[0].resourceMetrics[0].resource.attributes
|
|
217
|
+
expect(resourceAttrs).toContainEqual({ key: 'service.name', value: { stringValue: 'unknown_service' } })
|
|
218
|
+
expect(resourceAttrs.filter((kv) => kv.key === 'service.name')).toHaveLength(1)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('flush behavior', () => {
|
|
223
|
+
it('sends nothing until the flush interval elapses, then sends the window once', async () => {
|
|
224
|
+
const metrics = createMetrics({ flushIntervalMs: 5000 })
|
|
225
|
+
metrics.count('orders_created', 1)
|
|
226
|
+
expect(mockInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
227
|
+
|
|
228
|
+
await jest.advanceTimersByTimeAsync(5000)
|
|
229
|
+
expect(mockInstance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('does not send when the window is empty', async () => {
|
|
233
|
+
createMetrics({ flushIntervalMs: 5000 })
|
|
234
|
+
await jest.advanceTimersByTimeAsync(15000)
|
|
235
|
+
expect(mockInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
236
|
+
|
|
237
|
+
const metrics = createMetrics()
|
|
238
|
+
await metrics.flush()
|
|
239
|
+
expect(mockInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('resets the window after a successful flush — values do not leak into the next one', async () => {
|
|
243
|
+
const metrics = createMetrics()
|
|
244
|
+
metrics.count('orders_created', 5)
|
|
245
|
+
await metrics.flush()
|
|
246
|
+
metrics.count('orders_created', 2)
|
|
247
|
+
await metrics.flush()
|
|
248
|
+
|
|
249
|
+
const payloads = sentPayloads()
|
|
250
|
+
expect(payloads).toHaveLength(2)
|
|
251
|
+
expect(payloads[1].resourceMetrics[0].scopeMetrics[0].metrics[0].sum!.dataPoints[0].asDouble).toBe(2)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('retains the window and merges with new samples when the send says retry-later', async () => {
|
|
255
|
+
mockInstance._sendMetricsBatch
|
|
256
|
+
.mockResolvedValueOnce({ kind: 'retry-later', error: new Error('offline') })
|
|
257
|
+
.mockResolvedValue({ kind: 'ok' })
|
|
258
|
+
|
|
259
|
+
const metrics = createMetrics()
|
|
260
|
+
metrics.count('orders_created', 5)
|
|
261
|
+
await metrics.flush()
|
|
262
|
+
metrics.count('orders_created', 2)
|
|
263
|
+
await metrics.flush()
|
|
264
|
+
|
|
265
|
+
const payloads = sentPayloads()
|
|
266
|
+
expect(payloads).toHaveLength(2)
|
|
267
|
+
expect(payloads[1].resourceMetrics[0].scopeMetrics[0].metrics[0].sum!.dataPoints[0].asDouble).toBe(7)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('drops the window on a fatal send outcome', async () => {
|
|
271
|
+
mockInstance._sendMetricsBatch
|
|
272
|
+
.mockResolvedValueOnce({ kind: 'fatal', error: new Error('bad key') })
|
|
273
|
+
.mockResolvedValue({ kind: 'ok' })
|
|
274
|
+
|
|
275
|
+
const metrics = createMetrics()
|
|
276
|
+
metrics.count('orders_created', 5)
|
|
277
|
+
await metrics.flush()
|
|
278
|
+
metrics.count('orders_created', 2)
|
|
279
|
+
await metrics.flush()
|
|
280
|
+
|
|
281
|
+
expect(sentPayloads()[1].resourceMetrics[0].scopeMetrics[0].metrics[0].sum!.dataPoints[0].asDouble).toBe(2)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('keeps flushing on the interval', async () => {
|
|
285
|
+
const metrics = createMetrics({ flushIntervalMs: 5000 })
|
|
286
|
+
metrics.count('a', 1)
|
|
287
|
+
await jest.advanceTimersByTimeAsync(5000)
|
|
288
|
+
metrics.count('a', 1)
|
|
289
|
+
await jest.advanceTimersByTimeAsync(5000)
|
|
290
|
+
expect(mockInstance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('stops the timer and clears state on reset', async () => {
|
|
294
|
+
const metrics = createMetrics({ flushIntervalMs: 5000 })
|
|
295
|
+
metrics.count('a', 1)
|
|
296
|
+
metrics.reset()
|
|
297
|
+
await jest.advanceTimersByTimeAsync(20000)
|
|
298
|
+
expect(mockInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('drainWindow returns the built payload synchronously and resets the window', async () => {
|
|
302
|
+
const metrics = createMetrics()
|
|
303
|
+
metrics.count('orders_created', 3)
|
|
304
|
+
|
|
305
|
+
const payload = metrics.drainWindow()
|
|
306
|
+
expect(payload!.resourceMetrics[0].scopeMetrics[0].metrics[0].sum!.dataPoints[0].asDouble).toBe(3)
|
|
307
|
+
|
|
308
|
+
// The window was consumed: nothing left for the regular flush.
|
|
309
|
+
await metrics.flush()
|
|
310
|
+
expect(mockInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('retries a merged-back window on the next interval even with no new samples', async () => {
|
|
314
|
+
mockInstance._sendMetricsBatch
|
|
315
|
+
.mockResolvedValueOnce({ kind: 'retry-later', error: new Error('offline') })
|
|
316
|
+
.mockResolvedValue({ kind: 'ok' })
|
|
317
|
+
|
|
318
|
+
const metrics = createMetrics({ flushIntervalMs: 5000 })
|
|
319
|
+
metrics.count('orders_created', 5)
|
|
320
|
+
await jest.advanceTimersByTimeAsync(5000)
|
|
321
|
+
expect(mockInstance._sendMetricsBatch).toHaveBeenCalledTimes(1)
|
|
322
|
+
|
|
323
|
+
// No new captures — the retained window must not be stranded until shutdown.
|
|
324
|
+
await jest.advanceTimersByTimeAsync(5000)
|
|
325
|
+
expect(mockInstance._sendMetricsBatch).toHaveBeenCalledTimes(2)
|
|
326
|
+
const payload: OtlpMetricsPayload = mockInstance._sendMetricsBatch.mock.calls[1][0]
|
|
327
|
+
expect(payload.resourceMetrics[0].scopeMetrics[0].metrics[0].sum!.dataPoints[0].asDouble).toBe(5)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('drainWindow returns null when the window is empty', () => {
|
|
331
|
+
const metrics = createMetrics()
|
|
332
|
+
expect(metrics.drainWindow()).toBeNull()
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('guardrails and filtering', () => {
|
|
337
|
+
it('caps distinct series per window and warns once', async () => {
|
|
338
|
+
const metrics = createMetrics({ maxSeriesPerFlush: 2 })
|
|
339
|
+
metrics.count('a', 1)
|
|
340
|
+
metrics.count('b', 1)
|
|
341
|
+
metrics.count('c', 1)
|
|
342
|
+
metrics.count('d', 1)
|
|
343
|
+
metrics.count('a', 1) // existing series still aggregates
|
|
344
|
+
await metrics.flush()
|
|
345
|
+
|
|
346
|
+
const sent = sentMetrics()
|
|
347
|
+
expect(sent.map((m) => m.name).sort()).toEqual(['a', 'b'])
|
|
348
|
+
expect(sent.find((m) => m.name === 'a')!.sum!.dataPoints[0].asDouble).toBe(2)
|
|
349
|
+
expect((logger.warn as jest.Mock).mock.calls.length).toBe(1)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('drops non-finite values and negative counts', async () => {
|
|
353
|
+
const metrics = createMetrics()
|
|
354
|
+
metrics.count('a', NaN)
|
|
355
|
+
metrics.count('a', Infinity)
|
|
356
|
+
metrics.count('a', -1)
|
|
357
|
+
metrics.gauge('b', NaN)
|
|
358
|
+
metrics.histogram('c', -Infinity)
|
|
359
|
+
metrics.gauge('b', -5) // negative gauge is valid
|
|
360
|
+
await metrics.flush()
|
|
361
|
+
|
|
362
|
+
const sent = sentMetrics()
|
|
363
|
+
expect(sent).toHaveLength(1)
|
|
364
|
+
expect(sent[0].name).toBe('b')
|
|
365
|
+
expect(sent[0].gauge!.dataPoints[0].asDouble).toBe(-5)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('drops samples with an empty name', async () => {
|
|
369
|
+
const metrics = createMetrics()
|
|
370
|
+
metrics.count('', 1)
|
|
371
|
+
await metrics.flush()
|
|
372
|
+
expect(mockInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('applies beforeSend: transform, chain, drop, and throw', async () => {
|
|
376
|
+
const metrics = createMetrics({
|
|
377
|
+
beforeSend: [
|
|
378
|
+
(m) => (m.name === 'dropped' ? null : m),
|
|
379
|
+
(m) => ({ ...m, attributes: { ...m.attributes, processed: true } }),
|
|
380
|
+
],
|
|
381
|
+
})
|
|
382
|
+
metrics.count('kept', 1)
|
|
383
|
+
metrics.count('dropped', 1)
|
|
384
|
+
await metrics.flush()
|
|
385
|
+
|
|
386
|
+
const sent = sentMetrics()
|
|
387
|
+
expect(sent).toHaveLength(1)
|
|
388
|
+
expect(sent[0].name).toBe('kept')
|
|
389
|
+
expect(sent[0].sum!.dataPoints[0].attributes).toContainEqual({
|
|
390
|
+
key: 'processed',
|
|
391
|
+
value: { boolValue: true },
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('snapshots attributes at capture time — later caller mutation cannot corrupt the series', async () => {
|
|
396
|
+
const metrics = createMetrics()
|
|
397
|
+
const attributes = { plan: 'free' }
|
|
398
|
+
metrics.count('api_calls', 1, { attributes })
|
|
399
|
+
attributes.plan = 'pro'
|
|
400
|
+
metrics.count('api_calls', 1, { attributes })
|
|
401
|
+
await metrics.flush()
|
|
402
|
+
|
|
403
|
+
const sent = sentMetrics()
|
|
404
|
+
expect(sent[0].sum!.dataPoints).toHaveLength(2)
|
|
405
|
+
const values = sent[0].sum!.dataPoints.map((dp) => dp.attributes.find((a) => a.key === 'plan')!.value.stringValue)
|
|
406
|
+
expect(values.sort()).toEqual(['free', 'pro'])
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('warns once when a metric name is reused with a different type, but keeps both series', async () => {
|
|
410
|
+
const metrics = createMetrics()
|
|
411
|
+
metrics.count('throughput', 5)
|
|
412
|
+
metrics.gauge('throughput', 42)
|
|
413
|
+
metrics.gauge('throughput', 43)
|
|
414
|
+
await metrics.flush()
|
|
415
|
+
|
|
416
|
+
// Both series still ship — the warning is a dev-time hint, not a drop.
|
|
417
|
+
const sent = sentMetrics()
|
|
418
|
+
expect(sent.some((m) => m.sum)).toBe(true)
|
|
419
|
+
expect(sent.some((m) => m.gauge)).toBe(true)
|
|
420
|
+
const typeWarns = (logger.warn as jest.Mock).mock.calls.filter((c) => String(c[0]).includes('already used'))
|
|
421
|
+
expect(typeWarns).toHaveLength(1)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('drops counts made negative by beforeSend', async () => {
|
|
425
|
+
const metrics = createMetrics({
|
|
426
|
+
beforeSend: (m) => ({ ...m, value: -5 }),
|
|
427
|
+
})
|
|
428
|
+
metrics.count('a', 1)
|
|
429
|
+
await metrics.flush()
|
|
430
|
+
expect(mockInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('drops the sample when beforeSend throws', async () => {
|
|
434
|
+
const metrics = createMetrics({
|
|
435
|
+
beforeSend: () => {
|
|
436
|
+
throw new Error('boom')
|
|
437
|
+
},
|
|
438
|
+
})
|
|
439
|
+
metrics.count('a', 1)
|
|
440
|
+
await metrics.flush()
|
|
441
|
+
expect(mockInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('no-ops when the host is disabled or opted out', async () => {
|
|
445
|
+
const disabledInstance = createMockInstance({ isDisabled: true })
|
|
446
|
+
const disabled = createMetrics({}, disabledInstance)
|
|
447
|
+
disabled.count('a', 1)
|
|
448
|
+
await disabled.flush()
|
|
449
|
+
|
|
450
|
+
const optedOutInstance = createMockInstance({ optedOut: true })
|
|
451
|
+
const optedOut = createMetrics({}, optedOutInstance)
|
|
452
|
+
optedOut.count('a', 1)
|
|
453
|
+
await optedOut.flush()
|
|
454
|
+
|
|
455
|
+
expect(disabledInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
456
|
+
expect(optedOutInstance._sendMetricsBatch).not.toHaveBeenCalled()
|
|
457
|
+
})
|
|
458
|
+
})
|
|
459
|
+
})
|