|
1 | 1 | // <reference lib="deno.ns" /> |
2 | 2 |
|
| 3 | +import type { Envelope } from '@sentry/core'; |
| 4 | +import { createStackParser, forEachEnvelopeItem, nodeStackLineParser } from '@sentry/core'; |
3 | 5 | import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.212.0/assert/mod.ts'; |
4 | | -import { spy, stub } from 'https://deno.land/std@0.212.0/testing/mock.ts'; |
5 | | -import { FakeTime } from 'https://deno.land/std@0.212.0/testing/time.ts'; |
6 | | -import { denoRuntimeMetricsIntegration, metrics } from '../build/esm/index.js'; |
| 6 | +import { |
| 7 | + DenoClient, |
| 8 | + denoRuntimeMetricsIntegration, |
| 9 | + getCurrentScope, |
| 10 | + getDefaultIntegrations, |
| 11 | +} from '../build/esm/index.js'; |
| 12 | +import { makeTestTransport } from './transport.ts'; |
7 | 13 |
|
8 | | -const MOCK_MEMORY: Deno.MemoryUsage = { |
9 | | - rss: 50_000_000, |
10 | | - heapTotal: 30_000_000, |
11 | | - heapUsed: 20_000_000, |
12 | | - external: 1_000_000, |
13 | | -}; |
| 14 | +const DSN = 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507'; |
| 15 | + |
| 16 | +function delay(ms: number): Promise<void> { |
| 17 | + return new Promise(resolve => setTimeout(resolve, ms)); |
| 18 | +} |
14 | 19 |
|
15 | 20 | // deno-lint-ignore no-explicit-any |
16 | | -type AnyCall = { args: any[] }; |
| 21 | +type MetricItem = { name: string; type: string; value: number; unit?: string; attributes?: Record<string, any> }; |
| 22 | + |
| 23 | +async function collectMetrics( |
| 24 | + integrationOptions: Parameters<typeof denoRuntimeMetricsIntegration>[0] = {}, |
| 25 | +): Promise<MetricItem[]> { |
| 26 | + const envelopes: Envelope[] = []; |
| 27 | + |
| 28 | + // Hold a reference so we can call teardown() to stop the interval before the test ends. |
| 29 | + const metricsIntegration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 100, ...integrationOptions }); |
| 30 | + |
| 31 | + const client = new DenoClient({ |
| 32 | + dsn: DSN, |
| 33 | + integrations: [...getDefaultIntegrations({}), metricsIntegration], |
| 34 | + stackParser: createStackParser(nodeStackLineParser()), |
| 35 | + transport: makeTestTransport(envelope => { |
| 36 | + envelopes.push(envelope); |
| 37 | + }), |
| 38 | + }); |
| 39 | + |
| 40 | + client.init(); |
| 41 | + getCurrentScope().setClient(client); |
| 42 | + |
| 43 | + await delay(250); |
| 44 | + await client.flush(2000); |
| 45 | + |
| 46 | + // Stop the collection interval so Deno's leak detector doesn't flag it. |
| 47 | + metricsIntegration.teardown?.(); |
| 48 | + |
| 49 | + const items: MetricItem[] = []; |
| 50 | + for (const envelope of envelopes) { |
| 51 | + forEachEnvelopeItem(envelope, item => { |
| 52 | + const [headers, body] = item; |
| 53 | + if (headers.type === 'trace_metric') { |
| 54 | + // deno-lint-ignore no-explicit-any |
| 55 | + items.push(...(body as any).items); |
| 56 | + } |
| 57 | + }); |
| 58 | + } |
| 59 | + |
| 60 | + return items; |
| 61 | +} |
17 | 62 |
|
18 | 63 | Deno.test('denoRuntimeMetricsIntegration has the correct name', () => { |
19 | 64 | const integration = denoRuntimeMetricsIntegration(); |
20 | 65 | assertEquals(integration.name, 'DenoRuntimeMetrics'); |
21 | 66 | }); |
22 | 67 |
|
23 | | -Deno.test('starts a collection interval', () => { |
24 | | - using time = new FakeTime(); |
25 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
26 | | - const gaugeSpy = spy(metrics, 'gauge'); |
| 68 | +Deno.test('emits default memory metrics with correct shape', async () => { |
| 69 | + const items = await collectMetrics(); |
| 70 | + const names = items.map(i => i.name); |
27 | 71 |
|
28 | | - try { |
29 | | - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); |
30 | | - integration.setup!({} as never); |
| 72 | + assertEquals(names.includes('deno.runtime.mem.rss'), true); |
| 73 | + assertEquals(names.includes('deno.runtime.mem.heap_used'), true); |
| 74 | + assertEquals(names.includes('deno.runtime.mem.heap_total'), true); |
31 | 75 |
|
32 | | - assertEquals(gaugeSpy.calls.length, 0); |
33 | | - time.tick(1_000); |
34 | | - assertNotEquals(gaugeSpy.calls.length, 0); |
35 | | - } finally { |
36 | | - gaugeSpy.restore(); |
37 | | - } |
| 76 | + const rss = items.find(i => i.name === 'deno.runtime.mem.rss'); |
| 77 | + assertEquals(rss?.type, 'gauge'); |
| 78 | + assertEquals(rss?.unit, 'byte'); |
| 79 | + assertEquals(typeof rss?.value, 'number'); |
38 | 80 | }); |
39 | 81 |
|
40 | | -Deno.test('emits default memory metrics', () => { |
41 | | - using time = new FakeTime(); |
42 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
43 | | - const gaugeSpy = spy(metrics, 'gauge'); |
44 | | - |
45 | | - try { |
46 | | - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); |
47 | | - integration.setup!({} as never); |
48 | | - time.tick(1_000); |
49 | | - |
50 | | - const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); |
51 | | - assertEquals(names.includes('deno.runtime.mem.rss'), true); |
52 | | - assertEquals(names.includes('deno.runtime.mem.heap_used'), true); |
53 | | - assertEquals(names.includes('deno.runtime.mem.heap_total'), true); |
54 | | - } finally { |
55 | | - gaugeSpy.restore(); |
56 | | - } |
57 | | -}); |
| 82 | +Deno.test('emits uptime counter', async () => { |
| 83 | + const items = await collectMetrics(); |
| 84 | + const uptime = items.find(i => i.name === 'deno.runtime.process.uptime'); |
58 | 85 |
|
59 | | -Deno.test('emits correct memory values', () => { |
60 | | - using time = new FakeTime(); |
61 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
62 | | - const gaugeSpy = spy(metrics, 'gauge'); |
63 | | - |
64 | | - try { |
65 | | - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); |
66 | | - integration.setup!({} as never); |
67 | | - time.tick(1_000); |
68 | | - |
69 | | - const calls = gaugeSpy.calls as AnyCall[]; |
70 | | - const rssCall = calls.find(c => c.args[0] === 'deno.runtime.mem.rss'); |
71 | | - const heapUsedCall = calls.find(c => c.args[0] === 'deno.runtime.mem.heap_used'); |
72 | | - const heapTotalCall = calls.find(c => c.args[0] === 'deno.runtime.mem.heap_total'); |
73 | | - |
74 | | - assertEquals(rssCall?.args[1], 50_000_000); |
75 | | - assertEquals(heapUsedCall?.args[1], 20_000_000); |
76 | | - assertEquals(heapTotalCall?.args[1], 30_000_000); |
77 | | - } finally { |
78 | | - gaugeSpy.restore(); |
79 | | - } |
| 86 | + assertNotEquals(uptime, undefined); |
| 87 | + assertEquals(uptime?.type, 'counter'); |
| 88 | + assertEquals(uptime?.unit, 'second'); |
80 | 89 | }); |
81 | 90 |
|
82 | | -Deno.test('does not emit mem.external by default', () => { |
83 | | - using time = new FakeTime(); |
84 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
85 | | - const gaugeSpy = spy(metrics, 'gauge'); |
86 | | - |
87 | | - try { |
88 | | - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); |
89 | | - integration.setup!({} as never); |
90 | | - time.tick(1_000); |
91 | | - |
92 | | - const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); |
93 | | - assertEquals(names.includes('deno.runtime.mem.external'), false); |
94 | | - } finally { |
95 | | - gaugeSpy.restore(); |
96 | | - } |
| 91 | +Deno.test('does not emit mem.external by default', async () => { |
| 92 | + const items = await collectMetrics(); |
| 93 | + const names = items.map(i => i.name); |
| 94 | + assertEquals(names.includes('deno.runtime.mem.external'), false); |
97 | 95 | }); |
98 | 96 |
|
99 | | -Deno.test('emits mem.external when opted in', () => { |
100 | | - using time = new FakeTime(); |
101 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
102 | | - const gaugeSpy = spy(metrics, 'gauge'); |
| 97 | +Deno.test('emits mem.external when opted in', async () => { |
| 98 | + const items = await collectMetrics({ collect: { memExternal: true } }); |
| 99 | + const external = items.find(i => i.name === 'deno.runtime.mem.external'); |
103 | 100 |
|
104 | | - try { |
105 | | - const integration = denoRuntimeMetricsIntegration({ |
106 | | - collectionIntervalMs: 1_000, |
107 | | - collect: { memExternal: true }, |
108 | | - }); |
109 | | - integration.setup!({} as never); |
110 | | - time.tick(1_000); |
111 | | - |
112 | | - const calls = gaugeSpy.calls as AnyCall[]; |
113 | | - const externalCall = calls.find(c => c.args[0] === 'deno.runtime.mem.external'); |
114 | | - assertEquals(externalCall?.args[1], 1_000_000); |
115 | | - } finally { |
116 | | - gaugeSpy.restore(); |
117 | | - } |
| 101 | + assertNotEquals(external, undefined); |
| 102 | + assertEquals(external?.type, 'gauge'); |
| 103 | + assertEquals(external?.unit, 'byte'); |
118 | 104 | }); |
119 | 105 |
|
120 | | -Deno.test('emits uptime counter', () => { |
121 | | - using time = new FakeTime(); |
122 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
123 | | - const countSpy = spy(metrics, 'count'); |
124 | | - |
125 | | - try { |
126 | | - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); |
127 | | - integration.setup!({} as never); |
128 | | - time.tick(1_000); |
| 106 | +Deno.test('respects opt-out: skips uptime when disabled', async () => { |
| 107 | + const items = await collectMetrics({ collect: { uptime: false } }); |
| 108 | + const names = items.map(i => i.name); |
129 | 109 |
|
130 | | - const uptimeCall = (countSpy.calls as AnyCall[]).find(c => c.args[0] === 'deno.runtime.process.uptime'); |
131 | | - assertNotEquals(uptimeCall, undefined); |
132 | | - } finally { |
133 | | - countSpy.restore(); |
134 | | - } |
135 | | -}); |
136 | | - |
137 | | -Deno.test('respects opt-out: skips mem.rss when memRss is false', () => { |
138 | | - using time = new FakeTime(); |
139 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
140 | | - const gaugeSpy = spy(metrics, 'gauge'); |
141 | | - |
142 | | - try { |
143 | | - const integration = denoRuntimeMetricsIntegration({ |
144 | | - collectionIntervalMs: 1_000, |
145 | | - collect: { memRss: false }, |
146 | | - }); |
147 | | - integration.setup!({} as never); |
148 | | - time.tick(1_000); |
149 | | - |
150 | | - const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); |
151 | | - assertEquals(names.includes('deno.runtime.mem.rss'), false); |
152 | | - } finally { |
153 | | - gaugeSpy.restore(); |
154 | | - } |
| 110 | + assertEquals(names.includes('deno.runtime.mem.rss'), true); |
| 111 | + assertEquals(names.includes('deno.runtime.process.uptime'), false); |
155 | 112 | }); |
156 | 113 |
|
157 | | -Deno.test('skips uptime when uptime is false', () => { |
158 | | - using time = new FakeTime(); |
159 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
160 | | - const countSpy = spy(metrics, 'count'); |
| 114 | +Deno.test('attaches correct sentry.origin attribute', async () => { |
| 115 | + const items = await collectMetrics(); |
| 116 | + const rss = items.find(i => i.name === 'deno.runtime.mem.rss'); |
161 | 117 |
|
162 | | - try { |
163 | | - const integration = denoRuntimeMetricsIntegration({ |
164 | | - collectionIntervalMs: 1_000, |
165 | | - collect: { uptime: false }, |
166 | | - }); |
167 | | - integration.setup!({} as never); |
168 | | - time.tick(1_000); |
169 | | - |
170 | | - const uptimeCall = (countSpy.calls as AnyCall[]).find(c => c.args[0] === 'deno.runtime.process.uptime'); |
171 | | - assertEquals(uptimeCall, undefined); |
172 | | - } finally { |
173 | | - countSpy.restore(); |
174 | | - } |
175 | | -}); |
176 | | - |
177 | | -Deno.test('attaches correct sentry.origin attribute', () => { |
178 | | - using time = new FakeTime(); |
179 | | - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); |
180 | | - const gaugeSpy = spy(metrics, 'gauge'); |
181 | | - |
182 | | - try { |
183 | | - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); |
184 | | - integration.setup!({} as never); |
185 | | - time.tick(1_000); |
186 | | - |
187 | | - const calls = gaugeSpy.calls as AnyCall[]; |
188 | | - const rssCall = calls.find(c => c.args[0] === 'deno.runtime.mem.rss'); |
189 | | - assertEquals(rssCall?.args[2]?.attributes?.['sentry.origin'], 'auto.deno.runtime_metrics'); |
190 | | - } finally { |
191 | | - gaugeSpy.restore(); |
192 | | - } |
| 118 | + // Attributes in the serialized envelope are { type, value } objects. |
| 119 | + assertEquals(rss?.attributes?.['sentry.origin']?.value, 'auto.deno.runtime_metrics'); |
193 | 120 | }); |
0 commit comments