Skip to content

Commit 3303647

Browse files
committed
feat: support async api key fetching (browser SDK)
1 parent 4ad6c88 commit 3303647

3 files changed

Lines changed: 129 additions & 86 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperdx/browser': patch
3+
---
4+
5+
feat: support async api key fetching (browser SDK)

packages/browser/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ HyperDX.init({
2626

2727
#### Options
2828

29-
- `apiKey` - Your HyperDX Ingestion API Key.
29+
- `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).
3030
- `service` - The service name events will show up as in HyperDX.
3131
- `tracePropagationTargets` - A list of regex patterns to match against HTTP
3232
requests to link frontend and backend traces, it will add an additional
@@ -56,6 +56,23 @@ HyperDX.init({
5656

5757
## Additional Configuration
5858

59+
### Async API Key
60+
61+
If you need to fetch the API key from your backend, you can pass an async function to the `apiKey` option:
62+
63+
```js
64+
HyperDX.init({
65+
apiKey: async () => {
66+
const response = await fetch('/api/hyperdx-key');
67+
const data = await response.json();
68+
return data.apiKey;
69+
},
70+
service: 'my-frontend-app',
71+
});
72+
```
73+
74+
**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.
75+
5976
### Attach User Information or Metadata
6077

6178
Attaching user information will allow you to search/filter sessions and events

packages/browser/src/index.ts

Lines changed: 106 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ type ErrorBoundaryComponent = any; // TODO: Define ErrorBoundary type
1313
type Instrumentations = RumOtelWebConfig['instrumentations'];
1414
type IgnoreUrls = RumOtelWebConfig['ignoreUrls'];
1515

16+
type ApiKeyFn = () => Promise<string>;
17+
1618
type BrowserSDKConfig = {
1719
advancedNetworkCapture?: boolean;
18-
apiKey: string;
20+
apiKey: string | ApiKeyFn;
1921
blockClass?: string;
2022
captureConsole?: boolean; // deprecated
2123
consoleCapture?: boolean;
@@ -67,102 +69,121 @@ class Browser {
6769
tracePropagationTargets,
6870
url,
6971
otelResourceAttributes,
70-
}: BrowserSDKConfig) {
72+
}: BrowserSDKConfig): void {
7173
if (!hasWindow()) {
7274
return;
7375
}
7476

75-
if (apiKey == null) {
76-
console.warn('HyperDX: Missing apiKey, telemetry will not be saved.');
77-
} else if (apiKey === '') {
78-
console.warn(
79-
'HyperDX: apiKey is empty string, telemetry will not be saved.',
80-
);
81-
} else if (typeof apiKey !== 'string') {
82-
console.warn(
83-
'HyperDX: apiKey must be a string, telemetry will not be saved.',
84-
);
85-
}
86-
87-
const urlBase = url ?? URL_BASE;
88-
8977
this._advancedNetworkCapture = advancedNetworkCapture;
9078

91-
Rum.init({
92-
debug,
93-
url: `${urlBase}/v1/traces`,
94-
allowInsecureUrl: true,
95-
apiKey,
96-
applicationName: service,
97-
ignoreUrls,
98-
resourceAttributes: otelResourceAttributes,
99-
instrumentations: {
100-
visibility: true,
101-
console: captureConsole ?? consoleCapture ?? false,
102-
fetch: {
103-
...(tracePropagationTargets != null
104-
? {
105-
propagateTraceHeaderCorsUrls: tracePropagationTargets,
106-
}
107-
: {}),
108-
advancedNetworkCapture: () => this._advancedNetworkCapture,
109-
},
110-
xhr: {
111-
...(tracePropagationTargets != null
112-
? {
113-
propagateTraceHeaderCorsUrls: tracePropagationTargets,
114-
}
115-
: {}),
116-
advancedNetworkCapture: () => this._advancedNetworkCapture,
117-
},
118-
...instrumentations,
119-
},
120-
});
121-
122-
if (disableReplay !== true) {
123-
SessionRecorder.init({
124-
apiKey,
125-
blockClass,
79+
const initWithApiKey = (resolvedApiKey: string | undefined) => {
80+
if (resolvedApiKey == null) {
81+
console.warn('HyperDX: Missing apiKey, telemetry will not be saved.');
82+
} else if (resolvedApiKey === '') {
83+
console.warn(
84+
'HyperDX: apiKey is empty string, telemetry will not be saved.',
85+
);
86+
} else if (typeof resolvedApiKey !== 'string') {
87+
console.warn(
88+
'HyperDX: apiKey must be a string, telemetry will not be saved.',
89+
);
90+
}
91+
92+
const urlBase = url ?? URL_BASE;
93+
94+
Rum.init({
12695
debug,
127-
ignoreClass,
128-
maskAllInputs: maskAllInputs,
129-
maskTextClass: maskClass,
130-
maskTextSelector: maskAllText ? '*' : undefined,
131-
recordCanvas,
132-
sampling,
133-
url: `${urlBase}/v1/logs`,
96+
url: `${urlBase}/v1/traces`,
97+
allowInsecureUrl: true,
98+
apiKey: resolvedApiKey,
99+
applicationName: service,
100+
ignoreUrls,
101+
resourceAttributes: otelResourceAttributes,
102+
instrumentations: {
103+
visibility: true,
104+
console: captureConsole ?? consoleCapture ?? false,
105+
fetch: {
106+
...(tracePropagationTargets != null
107+
? {
108+
propagateTraceHeaderCorsUrls: tracePropagationTargets,
109+
}
110+
: {}),
111+
advancedNetworkCapture: () => this._advancedNetworkCapture,
112+
},
113+
xhr: {
114+
...(tracePropagationTargets != null
115+
? {
116+
propagateTraceHeaderCorsUrls: tracePropagationTargets,
117+
}
118+
: {}),
119+
advancedNetworkCapture: () => this._advancedNetworkCapture,
120+
},
121+
...instrumentations,
122+
},
134123
});
135-
}
136124

137-
const tracer = opentelemetry.trace.getTracer('@hyperdx/browser');
138-
139-
if (disableIntercom !== true) {
140-
resolveAsyncGlobal('Intercom')
141-
.then(() => {
142-
window.Intercom('onShow', () => {
143-
const sessionUrl = this.getSessionUrl();
144-
if (sessionUrl != null) {
145-
const metadata = {
146-
hyperdxSessionUrl: sessionUrl,
147-
};
148-
149-
// Use window.Intercom directly to avoid stale references
150-
window.Intercom('update', metadata);
151-
window.Intercom('trackEvent', 'HyperDX', metadata);
152-
153-
const now = Date.now();
154-
155-
const span = tracer.startSpan('intercom.onShow', {
156-
startTime: now,
157-
});
158-
span.setAttribute('component', 'intercom');
159-
span.end(now);
160-
}
125+
if (disableReplay !== true) {
126+
SessionRecorder.init({
127+
apiKey: resolvedApiKey,
128+
blockClass,
129+
debug,
130+
ignoreClass,
131+
maskAllInputs: maskAllInputs,
132+
maskTextClass: maskClass,
133+
maskTextSelector: maskAllText ? '*' : undefined,
134+
recordCanvas,
135+
sampling,
136+
url: `${urlBase}/v1/logs`,
137+
});
138+
}
139+
140+
const tracer = opentelemetry.trace.getTracer('@hyperdx/browser');
141+
142+
if (disableIntercom !== true) {
143+
resolveAsyncGlobal('Intercom')
144+
.then(() => {
145+
window.Intercom('onShow', () => {
146+
const sessionUrl = this.getSessionUrl();
147+
if (sessionUrl != null) {
148+
const metadata = {
149+
hyperdxSessionUrl: sessionUrl,
150+
};
151+
152+
// Use window.Intercom directly to avoid stale references
153+
window.Intercom('update', metadata);
154+
window.Intercom('trackEvent', 'HyperDX', metadata);
155+
156+
const now = Date.now();
157+
158+
const span = tracer.startSpan('intercom.onShow', {
159+
startTime: now,
160+
});
161+
span.setAttribute('component', 'intercom');
162+
span.end(now);
163+
}
164+
});
165+
})
166+
.catch(() => {
167+
// Ignore if intercom isn't installed or can't be used
161168
});
169+
}
170+
};
171+
172+
// Handle async apiKey resolution
173+
if (typeof apiKey === 'function') {
174+
apiKey()
175+
.then((resolved) => {
176+
initWithApiKey(resolved);
162177
})
163-
.catch(() => {
164-
// Ignore if intercom isn't installed or can't be used
178+
.catch((error) => {
179+
console.warn(
180+
'HyperDX: Failed to resolve apiKey from function:',
181+
error,
182+
);
183+
initWithApiKey(undefined);
165184
});
185+
} else {
186+
initWithApiKey(apiKey);
166187
}
167188
}
168189

0 commit comments

Comments
 (0)