Support custom resource attributes for Hyperdx frontend app's internal telemetry (#2067)

This commit is contained in:
Vineet Ahirkar 2026-04-14 17:40:20 -07:00 committed by GitHub
parent c4a1311e86
commit 6e9a553da2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 111 additions and 1 deletions

View file

@ -3,6 +3,7 @@ import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { NextAdapter } from 'next-query-params';
import { env } from 'next-runtime-env';
import randomUUID from 'crypto-randomuuid';
import { enableMapSet } from 'immer';
import { QueryParamProvider } from 'use-query-params';
@ -16,7 +17,7 @@ import {
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { DynamicFavicon } from '@/components/DynamicFavicon';
import { IS_LOCAL_MODE } from '@/config';
import { IS_LOCAL_MODE, parseResourceAttributes } from '@/config';
import {
DEFAULT_FONT_VAR,
FONT_VAR_MAP,
@ -136,12 +137,19 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
.then(res => res.json())
.then((_jsonData?: NextApiConfigResponseData) => {
if (_jsonData?.apiKey) {
const frontendAttrs = parseResourceAttributes(
env('NEXT_PUBLIC_OTEL_RESOURCE_ATTRIBUTES') ?? '',
);
HyperDX.init({
apiKey: _jsonData.apiKey,
consoleCapture: true,
maskAllInputs: true,
maskAllText: true,
// service.version is applied last so it always reflects the
// NEXT_PUBLIC_APP_VERSION and cannot be overridden by
// NEXT_PUBLIC_OTEL_RESOURCE_ATTRIBUTES.
otelResourceAttributes: {
...frontendAttrs,
'service.version': process.env.NEXT_PUBLIC_APP_VERSION,
},
service: _jsonData.serviceName,

View file

@ -0,0 +1,83 @@
import { parseResourceAttributes } from '@/config';
describe('parseResourceAttributes', () => {
it('parses a standard comma-separated string', () => {
const raw =
'service.namespace=observability,deployment.environment=prod,k8s.cluster.name=us-west-2';
expect(parseResourceAttributes(raw)).toEqual({
'service.namespace': 'observability',
'deployment.environment': 'prod',
'k8s.cluster.name': 'us-west-2',
});
});
it('returns an empty object for an empty string', () => {
expect(parseResourceAttributes('')).toEqual({});
});
it('handles a single key=value pair', () => {
expect(parseResourceAttributes('foo=bar')).toEqual({ foo: 'bar' });
});
it('handles values containing equals signs', () => {
expect(parseResourceAttributes('url=https://example.com?a=1')).toEqual({
url: 'https://example.com?a=1',
});
});
it('skips malformed entries without an equals sign', () => {
expect(parseResourceAttributes('good=value,badentry,ok=yes')).toEqual({
good: 'value',
ok: 'yes',
});
});
it('skips entries where key is empty (leading equals)', () => {
expect(parseResourceAttributes('=nokey,valid=value')).toEqual({
valid: 'value',
});
});
it('handles trailing commas gracefully', () => {
expect(parseResourceAttributes('a=1,b=2,')).toEqual({ a: '1', b: '2' });
});
it('handles leading commas gracefully', () => {
expect(parseResourceAttributes(',a=1,b=2')).toEqual({ a: '1', b: '2' });
});
it('allows empty values', () => {
expect(parseResourceAttributes('key=')).toEqual({ key: '' });
});
it('last value wins for duplicate keys', () => {
expect(parseResourceAttributes('k=first,k=second')).toEqual({
k: 'second',
});
});
it('decodes percent-encoded commas in values', () => {
expect(parseResourceAttributes('tags=a%2Cb%2Cc')).toEqual({
tags: 'a,b,c',
});
});
it('decodes percent-encoded equals in values', () => {
expect(parseResourceAttributes('expr=x%3D1')).toEqual({
expr: 'x=1',
});
});
it('decodes percent-encoded keys', () => {
expect(parseResourceAttributes('my%2Ekey=value')).toEqual({
'my.key': 'value',
});
});
it('round-trips values with both encoded commas and equals', () => {
expect(parseResourceAttributes('q=a%3D1%2Cb%3D2,other=plain')).toEqual({
q: 'a=1,b=2',
other: 'plain',
});
});
});

View file

@ -14,6 +14,25 @@ export const HDX_SERVICE_NAME =
process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME ?? 'hdx-oss-dev-app';
export const HDX_EXPORTER_ENABLED =
(process.env.HDX_EXPORTER_ENABLED ?? 'true') === 'true';
export function parseResourceAttributes(raw: string): Record<string, string> {
return raw
.split(',')
.filter(Boolean)
.reduce(
(acc, pair) => {
const idx = pair.indexOf('=');
if (idx > 0) {
acc[decodeURIComponent(pair.slice(0, idx))] = decodeURIComponent(
pair.slice(idx + 1),
);
}
return acc;
},
{} as Record<string, string>,
);
}
export const HDX_COLLECTOR_URL =
process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT ??
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??