Skip to content

Commit 1e4cc34

Browse files
authored
fix: Fix an issue where metrics could have NaN or infinite values. (#220)
## Summary Fix an issue where the `downlinkMax` metric could be infinite and prevent successful metric exports. Infinite/NaN would become `null` in JSON, and the collector will treat the entire request payload as invalid if there is a null metric payload. ## How did you test this change? <!-- Frontend - Leave a screencast or a screenshot to visually describe the changes. --> ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? -->
1 parent 872c1f7 commit 1e4cc34

File tree

4 files changed

+111
-13
lines changed

4 files changed

+111
-13
lines changed

sdk/highlight-run/src/client/index.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ import {
104104
setItem,
105105
setStorageMode,
106106
} from './utils/storage'
107-
import { getDefaultDataURLOptions } from './utils/utils'
107+
import { getDefaultDataURLOptions, isMetricSafeNumber } from './utils/utils'
108108
import { type HighlightClientRequestWorker } from './workers/highlight-client-worker'
109109
import { payloadToBase64 } from './utils/payload'
110110
import HighlightClientWorker from './workers/highlight-client-worker?worker&inline'
@@ -1107,18 +1107,17 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`,
11071107
value: payload.type.toString(),
11081108
})
11091109
}
1110-
Object.entries(payload).forEach(
1111-
([name, value]) =>
1112-
value &&
1113-
typeof value === 'number' &&
1110+
Object.entries(payload).forEach(([name, value]) => {
1111+
if (isMetricSafeNumber(value)) {
11141112
this.recordGauge({
11151113
name,
11161114
value: value as number,
11171115
category: MetricCategory.Performance,
11181116
group: window.location.href,
11191117
tags,
1120-
}),
1121-
)
1118+
})
1119+
}
1120+
})
11221121
},
11231122
this._recordingStartTime,
11241123
),
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { isMetricSafeNumber } from './utils'
2+
3+
describe('isMetricSafeNumber', () => {
4+
describe('valid numbers', () => {
5+
const validNumbers = [
6+
{ value: 42, description: 'positive integer' },
7+
{ value: 0, description: 'zero' },
8+
{ value: 1, description: 'one' },
9+
{ value: -42, description: 'negative integer' },
10+
{ value: -1, description: 'negative one' },
11+
{ value: 3.14, description: 'positive float' },
12+
{ value: 0.5, description: 'positive decimal' },
13+
{ value: 1.0, description: 'float one' },
14+
{ value: -3.14, description: 'negative float' },
15+
{ value: -0.5, description: 'negative decimal' },
16+
{ value: Number.MIN_VALUE, description: 'MIN_VALUE' },
17+
{ value: Number.EPSILON, description: 'EPSILON' },
18+
{ value: Number.MAX_SAFE_INTEGER, description: 'MAX_SAFE_INTEGER' },
19+
{ value: Number.MAX_VALUE, description: 'MAX_VALUE' },
20+
]
21+
22+
test.each(validNumbers)(
23+
'should return true for $description ($value)',
24+
({ value }) => {
25+
expect(isMetricSafeNumber(value)).toBe(true)
26+
},
27+
)
28+
})
29+
30+
describe('invalid numbers', () => {
31+
const invalidNumbers = [
32+
{ value: NaN, description: 'NaN' },
33+
{ value: Number.NaN, description: 'Number.NaN' },
34+
{ value: Infinity, description: 'Infinity' },
35+
{
36+
value: Number.POSITIVE_INFINITY,
37+
description: 'Number.POSITIVE_INFINITY',
38+
},
39+
{ value: -Infinity, description: 'negative Infinity' },
40+
{
41+
value: Number.NEGATIVE_INFINITY,
42+
description: 'Number.NEGATIVE_INFINITY',
43+
},
44+
]
45+
46+
test.each(invalidNumbers)(
47+
'should return false for $description',
48+
({ value }) => {
49+
expect(isMetricSafeNumber(value)).toBe(false)
50+
},
51+
)
52+
})
53+
54+
describe('non-number types', () => {
55+
const nonNumbers = [
56+
{ value: '42', description: 'numeric string' },
57+
{ value: '3.14', description: 'decimal string' },
58+
{ value: 'not a number', description: 'text string' },
59+
{ value: '', description: 'empty string' },
60+
{ value: true, description: 'boolean true' },
61+
{ value: false, description: 'boolean false' },
62+
{ value: {}, description: 'empty object' },
63+
{ value: { value: 42 }, description: 'object with properties' },
64+
{ value: [], description: 'empty array' },
65+
{ value: [1, 2, 3], description: 'array with numbers' },
66+
{ value: null, description: 'null' },
67+
{ value: undefined, description: 'undefined' },
68+
{ value: () => {}, description: 'arrow function' },
69+
{ value: function () {}, description: 'function declaration' },
70+
{ value: Symbol('test'), description: 'symbol' },
71+
{ value: BigInt(42), description: 'BigInt constructor' },
72+
{ value: 42n, description: 'BigInt literal' },
73+
{ value: new Date(), description: 'Date object' },
74+
{ value: new Date('2023-01-01'), description: 'Date with string' },
75+
{ value: /test/, description: 'RegExp literal' },
76+
{ value: new RegExp('test'), description: 'RegExp constructor' },
77+
]
78+
79+
test.each(nonNumbers)(
80+
'should return false for $description',
81+
({ value }) => {
82+
expect(isMetricSafeNumber(value)).toBe(false)
83+
},
84+
)
85+
})
86+
})

sdk/highlight-run/src/client/utils/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,16 @@ export function getDefaultDataURLOptions(): {
250250
quality: 0.6,
251251
}
252252
}
253+
254+
/**
255+
* Check that a value is safe to use as a metric.
256+
*
257+
* This means the number must be a number, be finite, and not NaN.
258+
* NaN and Infinity serialize to null, which produces an invalid metric
259+
* payload.
260+
* @param value The value to check.
261+
* @returns True if the value is safe to use as a metric, false otherwise.
262+
*/
263+
export function isMetricSafeNumber(value: unknown): boolean {
264+
return typeof value === 'number' && !isNaN(value) && isFinite(value)
265+
}

sdk/highlight-run/src/sdk/observe.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import { recordException } from '../client/otel/recordException'
7979
import { ObserveOptions } from '../client/types/observe'
8080
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
8181
import { MeterProvider } from '@opentelemetry/sdk-metrics'
82+
import { isMetricSafeNumber } from '../client/utils/utils'
8283

8384
export class ObserveSDK implements Observe {
8485
/** Verbose project ID that is exposed to users. Legacy users may still be using ints. */
@@ -602,16 +603,15 @@ export class ObserveSDK implements Observe {
602603
}
603604
Object.entries(payload)
604605
.filter(([name]) => name !== 'relativeTimestamp')
605-
.forEach(
606-
([name, value]) =>
607-
value &&
608-
typeof value === 'number' &&
606+
.forEach(([name, value]) => {
607+
if (isMetricSafeNumber(value)) {
609608
this.recordGauge({
610609
name,
611610
value: value as number,
612611
attributes,
613-
}),
614-
)
612+
})
613+
}
614+
})
615615
}, new Date().getTime())
616616

617617
const { getDeviceDetails } = getPerformanceMethods()

0 commit comments

Comments
 (0)