diff --git a/.changeset/afraid-ladybugs-develop.md b/.changeset/afraid-ladybugs-develop.md new file mode 100644 index 00000000..c1960d9a --- /dev/null +++ b/.changeset/afraid-ladybugs-develop.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/browser': patch +--- + +feat: support async api key fetching (browser SDK) diff --git a/packages/browser/README.md b/packages/browser/README.md index cd5f4109..acf71f5a 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -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 @@ -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 diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 4c7bb4c3..bfe76527 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -13,9 +13,11 @@ type ErrorBoundaryComponent = any; // TODO: Define ErrorBoundary type type Instrumentations = RumOtelWebConfig['instrumentations']; type IgnoreUrls = RumOtelWebConfig['ignoreUrls']; +type ApiKeyFn = () => Promise; + type BrowserSDKConfig = { advancedNetworkCapture?: boolean; - apiKey: string; + apiKey: string | ApiKeyFn; blockClass?: string; captureConsole?: boolean; // deprecated consoleCapture?: boolean; @@ -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); }) - .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); } }