Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/afraid-ladybugs-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/browser': patch
---

feat: support async api key fetching (browser SDK)
19 changes: 18 additions & 1 deletion packages/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ HyperDX.init({

#### Options

- `apiKey` - Your HyperDX Ingestion API Key.
- `apiKey` - Your HyperDX Ingestion API Key. Can be a string or an async function that returns a string (useful for fetching the key from your backend).
- `service` - The service name events will show up as in HyperDX.
- `tracePropagationTargets` - A list of regex patterns to match against HTTP
requests to link frontend and backend traces, it will add an additional
Expand Down Expand Up @@ -56,6 +56,23 @@ HyperDX.init({

## Additional Configuration

### Async API Key

If you need to fetch the API key from your backend, you can pass an async function to the `apiKey` option:

```js
HyperDX.init({
apiKey: async () => {
const response = await fetch('/api/hyperdx-key');
const data = await response.json();
return data.apiKey;
},
service: 'my-frontend-app',
});
```

**Note**: When using an async function for `apiKey`, any events that occur before the API key resolves will not be captured. The SDK initialization is deferred until the API key is available.

### Attach User Information or Metadata

Attaching user information will allow you to search/filter sessions and events
Expand Down
191 changes: 106 additions & 85 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ type ErrorBoundaryComponent = any; // TODO: Define ErrorBoundary type
type Instrumentations = RumOtelWebConfig['instrumentations'];
type IgnoreUrls = RumOtelWebConfig['ignoreUrls'];

type ApiKeyFn = () => Promise<string>;

type BrowserSDKConfig = {
advancedNetworkCapture?: boolean;
apiKey: string;
apiKey: string | ApiKeyFn;
blockClass?: string;
captureConsole?: boolean; // deprecated
consoleCapture?: boolean;
Expand Down Expand Up @@ -67,102 +69,121 @@ class Browser {
tracePropagationTargets,
url,
otelResourceAttributes,
}: BrowserSDKConfig) {
}: BrowserSDKConfig): void {
if (!hasWindow()) {
return;
}

if (apiKey == null) {
console.warn('HyperDX: Missing apiKey, telemetry will not be saved.');
} else if (apiKey === '') {
console.warn(
'HyperDX: apiKey is empty string, telemetry will not be saved.',
);
} else if (typeof apiKey !== 'string') {
console.warn(
'HyperDX: apiKey must be a string, telemetry will not be saved.',
);
}

const urlBase = url ?? URL_BASE;

this._advancedNetworkCapture = advancedNetworkCapture;

Rum.init({
debug,
url: `${urlBase}/v1/traces`,
allowInsecureUrl: true,
apiKey,
applicationName: service,
ignoreUrls,
resourceAttributes: otelResourceAttributes,
instrumentations: {
visibility: true,
console: captureConsole ?? consoleCapture ?? false,
fetch: {
...(tracePropagationTargets != null
? {
propagateTraceHeaderCorsUrls: tracePropagationTargets,
}
: {}),
advancedNetworkCapture: () => this._advancedNetworkCapture,
},
xhr: {
...(tracePropagationTargets != null
? {
propagateTraceHeaderCorsUrls: tracePropagationTargets,
}
: {}),
advancedNetworkCapture: () => this._advancedNetworkCapture,
},
...instrumentations,
},
});

if (disableReplay !== true) {
SessionRecorder.init({
apiKey,
blockClass,
const initWithApiKey = (resolvedApiKey: string | undefined) => {
if (resolvedApiKey == null) {
console.warn('HyperDX: Missing apiKey, telemetry will not be saved.');
} else if (resolvedApiKey === '') {
console.warn(
'HyperDX: apiKey is empty string, telemetry will not be saved.',
);
} else if (typeof resolvedApiKey !== 'string') {
console.warn(
'HyperDX: apiKey must be a string, telemetry will not be saved.',
);
}

const urlBase = url ?? URL_BASE;

Rum.init({
debug,
ignoreClass,
maskAllInputs: maskAllInputs,
maskTextClass: maskClass,
maskTextSelector: maskAllText ? '*' : undefined,
recordCanvas,
sampling,
url: `${urlBase}/v1/logs`,
url: `${urlBase}/v1/traces`,
allowInsecureUrl: true,
apiKey: resolvedApiKey,
applicationName: service,
ignoreUrls,
resourceAttributes: otelResourceAttributes,
instrumentations: {
visibility: true,
console: captureConsole ?? consoleCapture ?? false,
fetch: {
...(tracePropagationTargets != null
? {
propagateTraceHeaderCorsUrls: tracePropagationTargets,
}
: {}),
advancedNetworkCapture: () => this._advancedNetworkCapture,
},
xhr: {
...(tracePropagationTargets != null
? {
propagateTraceHeaderCorsUrls: tracePropagationTargets,
}
: {}),
advancedNetworkCapture: () => this._advancedNetworkCapture,
},
...instrumentations,
},
});
}

const tracer = opentelemetry.trace.getTracer('@hyperdx/browser');

if (disableIntercom !== true) {
resolveAsyncGlobal('Intercom')
.then(() => {
window.Intercom('onShow', () => {
const sessionUrl = this.getSessionUrl();
if (sessionUrl != null) {
const metadata = {
hyperdxSessionUrl: sessionUrl,
};

// Use window.Intercom directly to avoid stale references
window.Intercom('update', metadata);
window.Intercom('trackEvent', 'HyperDX', metadata);

const now = Date.now();

const span = tracer.startSpan('intercom.onShow', {
startTime: now,
});
span.setAttribute('component', 'intercom');
span.end(now);
}
if (disableReplay !== true) {
SessionRecorder.init({
apiKey: resolvedApiKey,
blockClass,
debug,
ignoreClass,
maskAllInputs: maskAllInputs,
maskTextClass: maskClass,
maskTextSelector: maskAllText ? '*' : undefined,
recordCanvas,
sampling,
url: `${urlBase}/v1/logs`,
});
}

const tracer = opentelemetry.trace.getTracer('@hyperdx/browser');

if (disableIntercom !== true) {
resolveAsyncGlobal('Intercom')
.then(() => {
window.Intercom('onShow', () => {
const sessionUrl = this.getSessionUrl();
if (sessionUrl != null) {
const metadata = {
hyperdxSessionUrl: sessionUrl,
};

// Use window.Intercom directly to avoid stale references
window.Intercom('update', metadata);
window.Intercom('trackEvent', 'HyperDX', metadata);

const now = Date.now();

const span = tracer.startSpan('intercom.onShow', {
startTime: now,
});
span.setAttribute('component', 'intercom');
span.end(now);
}
});
})
.catch(() => {
// Ignore if intercom isn't installed or can't be used
});
}
};

// Handle async apiKey resolution
if (typeof apiKey === 'function') {
apiKey()
.then((resolved) => {
initWithApiKey(resolved);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not going to work because of

if (inited) {
diag.warn('Rum already init()ed.');
return;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also are there any implications of initialising > 1 times?
I thought about calling Rum.deinit() before reinitializing, but not sure about the impact

Copy link
Member Author

@wrn14897 wrn14897 Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my understanding is the initWithApiKey will be called once the api key is fetched. for reinit, I don't think the api is exposed on the browser sdk side. what's the use case?

})
.catch(() => {
// Ignore if intercom isn't installed or can't be used
.catch((error) => {
console.warn(
'HyperDX: Failed to resolve apiKey from function:',
error,
);
initWithApiKey(undefined);
});
} else {
initWithApiKey(apiKey);
}
}

Expand Down