Skip to content

Commit 0164017

Browse files
Merge branch 'main' into dependabot/npm_and_yarn/picomatch-2.3.2
2 parents 12eb051 + 976faaa commit 0164017

29 files changed

Lines changed: 677 additions & 214 deletions

.github/workflows/release.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ jobs:
3737
env:
3838
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3939
NPM_CONFIG_PROVENANCE: true
40+
# Re-checkout to restore changeset files consumed by the changesets action above.
41+
# Without this, `changeset version --snapshot next` finds no changesets and
42+
# publishes full release versions to the `next` tag instead of snapshots.
43+
- name: Restore checkout for snapshot
44+
if: steps.changesets.outputs.published != 'true' && steps.changesets.outputs.hasChangesets == 'true'
45+
uses: actions/checkout@v4
46+
- name: Install dependencies for snapshot
47+
if: steps.changesets.outputs.published != 'true' && steps.changesets.outputs.hasChangesets == 'true'
48+
run: yarn
4049
- name: Publish snapshot to next tag
4150
if: steps.changesets.outputs.published != 'true' && steps.changesets.outputs.hasChangesets == 'true'
4251
run: yarn run release:next

lint-staged.config.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ module.exports = {
22
'**/*.{ts,tsx}': (files) => {
33
const cmds = [`prettier --write --ignore-unknown ${files.join(' ')}`];
44

5-
// `otel-web` package does not have an eslint config yet
6-
const lintable = files.filter((f) => !f.includes('/packages/otel-web/'));
5+
// These packages do not have an eslint config yet
6+
const lintable = files.filter(
7+
(f) =>
8+
!f.includes('/packages/otel-web/') &&
9+
!f.includes('/packages/session-recorder/'),
10+
);
711

812
if (lintable.length) {
913
cmds.push(`eslint --fix ${lintable.join(' ')}`);

packages/browser/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# @hyperdx/browser
22

3+
## 0.23.0
4+
5+
### Minor Changes
6+
7+
- 057f3b9: Emit FCP and TTFB Core Web Vitals from the browser RUM SDK. The bundled
8+
`web-vitals` library already exports `onFCP` and `onTTFB`; `initWebVitals`
9+
now registers callbacks for them alongside the existing LCP / INP / CLS / FID
10+
ones. Each new metric lands as a `webvitals` span with the value on a single
11+
attribute (`fcp` or `ttfb`), matching the existing pattern.
12+
13+
### Patch Changes
14+
15+
- Updated dependencies [057f3b9]
16+
- @hyperdx/otel-web@0.17.0
17+
- @hyperdx/otel-web-session-recorder@1.0.0
18+
319
## 0.22.1
420

521
### Patch Changes

packages/browser/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "@hyperdx/browser",
3-
"version": "0.22.1",
3+
"version": "0.23.0",
44
"license": "Apache-2.0",
55
"main": "build/index.js",
66
"types": "build/index.d.ts",
77
"dependencies": {
8-
"@hyperdx/otel-web": "0.16.4",
9-
"@hyperdx/otel-web-session-recorder": "0.16.2"
8+
"@hyperdx/otel-web": "0.17.0",
9+
"@hyperdx/otel-web-session-recorder": "1.0.0"
1010
},
1111
"devDependencies": {
1212
"@rollup/plugin-commonjs": "^24.1.0",

packages/instrumentation-exception/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @hyperdx/instrumentation-exception
22

3+
## 0.2.0
4+
5+
### Minor Changes
6+
7+
- 65be0bd: Improve how instrumation-exception stringifies properties like arrays and objects
8+
39
## 0.1.0
410

511
### Minor Changes

packages/instrumentation-exception/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@hyperdx/instrumentation-exception",
33
"author": "Warren <warren@users.noreply.github.com>",
44
"license": "Apache-2.0",
5-
"version": "0.1.0",
5+
"version": "0.2.0",
66
"homepage": "https://www.hyperdx.io",
77
"repository": {
88
"type": "git",

packages/instrumentation-exception/src/browser/instrumentations/HyperDXErrorInstrumentation.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
import * as shimmer from 'shimmer';
77

88
import { recordException } from '../';
9-
import { getElementXPath, limitLen } from './utils';
9+
import { getElementXPath, limitLen, stringifyValue } from './utils';
1010

1111
// FIXME take timestamps from events?
1212

@@ -17,14 +17,6 @@ function useful(s) {
1717
return s && s.trim() !== '' && !s.startsWith('[object') && s !== 'error';
1818
}
1919

20-
function stringifyValue(value: unknown) {
21-
if (value === undefined) {
22-
return '(undefined)';
23-
}
24-
25-
return value.toString();
26-
}
27-
2820
function addStackIfUseful(span: Span, err: Error) {
2921
if (err && err.stack && useful(err.stack)) {
3022
span.setAttribute(
@@ -210,7 +202,7 @@ export class HyperDXErrorInstrumentation extends InstrumentationBase {
210202
firstError,
211203
);
212204
} else {
213-
this.hdxReportString(source, stringifyValue(arg)); // FIXME or JSON.stringify?
205+
this.hdxReportString(source, stringifyValue(arg));
214206
}
215207
}
216208
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { stringifyValue } from '../utils';
2+
3+
describe('stringifyValue', () => {
4+
it('should JSON.stringify plain objects instead of producing [object Object]', () => {
5+
const obj = { errorId: 'abc-123', details: 'something broke' };
6+
const result = stringifyValue(obj);
7+
8+
expect(result).not.toContain('[object Object]');
9+
expect(result).toContain('errorId');
10+
expect(result).toContain('abc-123');
11+
expect(result).toBe('{"errorId":"abc-123","details":"something broke"}');
12+
});
13+
14+
it('should handle nested objects', () => {
15+
const result = stringifyValue({ outer: { inner: 'value' } });
16+
17+
expect(result).not.toContain('[object Object]');
18+
expect(result).toContain('inner');
19+
expect(result).toBe('{"outer":{"inner":"value"}}');
20+
});
21+
22+
it('should handle null', () => {
23+
expect(stringifyValue(null)).toBe('null');
24+
});
25+
26+
it('should handle undefined', () => {
27+
expect(stringifyValue(undefined)).toBe('(undefined)');
28+
});
29+
30+
it('should use error.message for Error objects instead of JSON.stringify', () => {
31+
const err = new Error('something failed');
32+
const result = stringifyValue(err);
33+
34+
// JSON.stringify(err) would produce "{}" because Error properties are non-enumerable
35+
expect(result).not.toBe('{}');
36+
expect(result).toContain('something failed');
37+
});
38+
39+
it('should handle Error objects with no message', () => {
40+
const err = new Error();
41+
const result = stringifyValue(err);
42+
43+
// Should fall back to toString() which gives "Error"
44+
expect(result).toBeTruthy();
45+
});
46+
47+
it('should return empty string for empty objects', () => {
48+
expect(stringifyValue({})).toBe('');
49+
});
50+
51+
it('should return empty string for empty arrays', () => {
52+
expect(stringifyValue([])).toBe('');
53+
});
54+
55+
it('should handle non-empty arrays', () => {
56+
const result = stringifyValue([1, 2, 3]);
57+
58+
expect(result).not.toContain('[object Object]');
59+
expect(result).toBe('[1,2,3]');
60+
});
61+
62+
it('should handle strings as-is', () => {
63+
expect(stringifyValue('hello')).toBe('hello');
64+
});
65+
66+
it('should handle numbers', () => {
67+
expect(stringifyValue(42)).toBe('42');
68+
});
69+
70+
it('should handle booleans', () => {
71+
expect(stringifyValue(true)).toBe('true');
72+
expect(stringifyValue(false)).toBe('false');
73+
});
74+
75+
it('should handle objects with circular references gracefully', () => {
76+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
77+
const obj: any = { a: 1 };
78+
obj.self = obj;
79+
80+
// Should not throw, falls back to toString()
81+
const result = stringifyValue(obj);
82+
expect(result).toBeDefined();
83+
});
84+
85+
it('should produce correct output for the reported console.error scenario', () => {
86+
// Simulates: console.error('FatalErrorBoundary caught an error', { errorId, error, errorInfo })
87+
// The hdxReport method joins stringifyValue results with spaces
88+
const args = [
89+
'FatalErrorBoundary caught an error',
90+
{
91+
errorId: 'err-001',
92+
error: 'TypeError: Cannot read properties of null',
93+
errorInfo: { componentStack: 'at App' },
94+
},
95+
];
96+
97+
const message = args.map((x) => stringifyValue(x)).join(' ');
98+
99+
expect(message).not.toContain('[object Object]');
100+
expect(message).toContain('FatalErrorBoundary caught an error');
101+
expect(message).toContain('errorId');
102+
expect(message).toContain('err-001');
103+
});
104+
});

packages/instrumentation-exception/src/browser/instrumentations/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
export function stringifyValue(value: unknown): string {
2+
if (value === undefined) {
3+
return '(undefined)';
4+
}
5+
6+
if (value === null) {
7+
return 'null';
8+
}
9+
10+
if (value instanceof Error) {
11+
return value.message || value.toString();
12+
}
13+
14+
if (typeof value === 'object') {
15+
try {
16+
const result = JSON.stringify(value);
17+
// Empty objects/arrays carry no useful error info
18+
if (result === '{}' || result === '[]') {
19+
return '';
20+
}
21+
return result;
22+
} catch (e) {
23+
return value.toString();
24+
}
25+
}
26+
27+
return value.toString();
28+
}
29+
130
export function limitLen(s: string, cap: number): string {
231
if (s.length > cap) {
332
return s.substring(0, cap);

packages/node-opentelemetry/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# @hyperdx/node-opentelemetry
22

3+
## 0.10.4
4+
5+
### Patch Changes
6+
7+
- Updated dependencies [65be0bd]
8+
- @hyperdx/instrumentation-exception@0.2.0
9+
310
## 0.10.3
411

512
### Patch Changes

0 commit comments

Comments
 (0)