From 14dcc782512a5fad09d4743184088a07e001b27f Mon Sep 17 00:00:00 2001 From: ethella Date: Thu, 5 Mar 2026 14:55:33 -0800 Subject: [PATCH 1/3] Add three layers storage and datadog --- packages/@magic-ext/oauth2/src/index.ts | 96 +++++++++---- packages/@magic-ext/oauth2/src/logger.ts | 51 +++++++ packages/@magic-ext/oauth2/src/storage.ts | 161 ++++++++++++++++++++++ 3 files changed, 282 insertions(+), 26 deletions(-) create mode 100644 packages/@magic-ext/oauth2/src/logger.ts create mode 100644 packages/@magic-ext/oauth2/src/storage.ts diff --git a/packages/@magic-ext/oauth2/src/index.ts b/packages/@magic-ext/oauth2/src/index.ts index 4f25a7212..e1e4de4c0 100644 --- a/packages/@magic-ext/oauth2/src/index.ts +++ b/packages/@magic-ext/oauth2/src/index.ts @@ -18,6 +18,8 @@ import { OAuthGetResultEventHandlers, } from '@magic-sdk/types'; import { createCryptoChallenge } from './crypto'; +import { storageWrite, storageRead, storageRemove } from './storage'; +import { logger } from './logger'; const PKCE_STORAGE_KEY = 'magic_oauth_pkce_verifier'; @@ -76,9 +78,28 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { if (successResult?.pkceMetadata) { // New path: store codeVerifier + all OAuth metadata at the SDK (parent page) level. - // sessionStorage persists across same-tab redirects but never enters the iframe. - sessionStorage.setItem(PKCE_STORAGE_KEY, JSON.stringify({ codeVerifier, ...successResult.pkceMetadata })); - localStorage.setItem(PKCE_STORAGE_KEY, JSON.stringify({ codeVerifier, ...successResult.pkceMetadata })); + // Written to sessionStorage, localStorage, and IndexedDB for maximum durability. + const writeResult = await storageWrite( + PKCE_STORAGE_KEY, + JSON.stringify({ codeVerifier, ...successResult.pkceMetadata }), + ); + + logger.info('oauth2.pkce.stored', { + pkce: { + provider: configuration.provider, + storageLayers: writeResult, + allLayersSucceeded: writeResult.sessionStorage && writeResult.localStorage && writeResult.indexedDB, + }, + }); + + if (!writeResult.sessionStorage || !writeResult.localStorage || !writeResult.indexedDB) { + logger.warn('oauth2.pkce.partial_write', { + pkce: { + provider: configuration.provider, + storageLayers: writeResult, + }, + }); + } } if (successResult?.oauthAuthoriationURI) { @@ -199,21 +220,15 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { private getResult(configuration: OAuthVerificationConfiguration, queryString: string) { const { showMfaModal } = configuration; - const { hasStateMismatch, clientMetadata } = this.retrievePKCEMetadata(queryString); - - const requestPayload = this.utils.createJsonRpcRequestPayload(OAuthPayloadMethods.Verify, [ - { - authorizationResponseParams: queryString, - magicApiKey: this.sdk.apiKey, - platform: 'web', - showUI: showMfaModal, - ...configuration, - ...(clientMetadata ? { clientMetadata } : {}), - }, - ]); + // requestPayload is assigned inside the async callback once PKCE metadata is retrieved. + // It is only accessed by the MFA intermediary closures below, which cannot fire until + // the server sends an MFA challenge — always after requestPayload has been assigned. + let requestPayload: ReturnType; const promiEvent = this.utils.createPromiEvent( async (resolve, reject) => { + const { hasStateMismatch, clientMetadata } = await this.retrievePKCEMetadata(queryString); + if (!clientMetadata) { return reject( this.createError( @@ -234,6 +249,17 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { ); } + requestPayload = this.utils.createJsonRpcRequestPayload(OAuthPayloadMethods.Verify, [ + { + authorizationResponseParams: queryString, + magicApiKey: this.sdk.apiKey, + platform: 'web', + showUI: showMfaModal, + ...configuration, + clientMetadata, + }, + ]); + const getResultRequest = this.request( requestPayload, ); @@ -318,25 +344,36 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { } } - private retrievePKCEMetadata(queryString: string): { + private async retrievePKCEMetadata(queryString: string): Promise<{ clientMetadata?: Record; hasStateMismatch: boolean; - } { + }> { let hasStateMismatch = false; // Retrieve and immediately clear the full PKCE metadata stored at SDK level. - const storedInSession = sessionStorage.getItem(PKCE_STORAGE_KEY); - const storedInLocal = localStorage.getItem(PKCE_STORAGE_KEY); - sessionStorage.removeItem(PKCE_STORAGE_KEY); - localStorage.removeItem(PKCE_STORAGE_KEY); + // Reads from sessionStorage → localStorage → IndexedDB (first non-null wins). + const { value: stored, source: storageSource } = await storageRead(PKCE_STORAGE_KEY); + await storageRemove(PKCE_STORAGE_KEY); // clientMetadata contains { codeVerifier, state, redirectUri, appID, provider }. // Forwarding it lets the embedded-wallet verify handler skip its iframe storage entirely. // When absent (old embedded-wallet path), the handler falls back to its stored metadata. - const clientMetadata = storedInSession - ? (JSON.parse(storedInSession) as Record) - : storedInLocal - ? (JSON.parse(storedInLocal) as Record) - : undefined; + const clientMetadata = stored ? (JSON.parse(stored) as Record) : undefined; + + if (!clientMetadata) { + logger.error('oauth2.pkce.missing', { + pkce: { + storageSource, + referrer: typeof document !== 'undefined' ? document.referrer : undefined, + }, + }); + } else { + logger.info('oauth2.pkce.retrieved', { + pkce: { + storageSource, + provider: clientMetadata.provider, + }, + }); + } // State verification for the new PKCE path. // The extension generated the state, so it verifies it here — before any RPC call — as CSRF protection. @@ -345,6 +382,13 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { const returnedState = new URLSearchParams(queryString).get('state'); if (!returnedState || returnedState !== clientMetadata.state) { hasStateMismatch = true; + logger.error('oauth2.pkce.state_mismatch', { + pkce: { + storageSource, + provider: clientMetadata.provider, + hasReturnedState: !!returnedState, + }, + }); } } diff --git a/packages/@magic-ext/oauth2/src/logger.ts b/packages/@magic-ext/oauth2/src/logger.ts new file mode 100644 index 000000000..e25ea9fa3 --- /dev/null +++ b/packages/@magic-ext/oauth2/src/logger.ts @@ -0,0 +1,51 @@ +/** + * Lightweight Datadog logger for the oauth2 extension. + * + * Posts directly to the Datadog browser-logs HTTP intake endpoint using fetch — + * no SDK dependency required. The client token is browser-safe (write-only). + * + * Replace DATADOG_CLIENT_TOKEN with the actual Magic Datadog client token + * (starts with "pub...") before deploying. + */ + +// TODO: replace with the real Magic Datadog browser client token +const DATADOG_CLIENT_TOKEN = '__DATADOG_CLIENT_TOKEN__'; +const DATADOG_INTAKE_URL = 'https://browser-intake-datadoghq.com/api/v2/logs'; +const SERVICE = 'magic-oauth2-extension'; + +export type LogContext = Record; + +function send(status: 'info' | 'warn' | 'error', message: string, context: LogContext = {}): void { + if (typeof fetch === 'undefined' || !DATADOG_CLIENT_TOKEN || DATADOG_CLIENT_TOKEN === '__DATADOG_CLIENT_TOKEN__') { + return; + } + + const entry = { + ddsource: 'browser', + service: SERVICE, + status, + message, + date: Date.now(), + 'http.useragent': typeof navigator !== 'undefined' ? navigator.userAgent : undefined, + 'page.origin': typeof window !== 'undefined' ? window.location.origin : undefined, + ...context, + }; + + const url = `${DATADOG_INTAKE_URL}?dd-api-key=${DATADOG_CLIENT_TOKEN}&ddsource=browser&service=${SERVICE}`; + + // Fire-and-forget. keepalive ensures the request survives page navigations (e.g. the OAuth redirect). + fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify([entry]), + keepalive: true, + }).catch(() => { + // never let logging errors surface to the caller + }); +} + +export const logger = { + info: (message: string, context?: LogContext) => send('info', message, context), + warn: (message: string, context?: LogContext) => send('warn', message, context), + error: (message: string, context?: LogContext) => send('error', message, context), +}; diff --git a/packages/@magic-ext/oauth2/src/storage.ts b/packages/@magic-ext/oauth2/src/storage.ts new file mode 100644 index 000000000..abe5f4e2e --- /dev/null +++ b/packages/@magic-ext/oauth2/src/storage.ts @@ -0,0 +1,161 @@ +import { logger } from './logger'; + +const IDB_DB_NAME = 'magic_oauth_db'; +const IDB_DB_VERSION = 1; +const IDB_STORE_NAME = 'pkce_store'; + +function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(IDB_DB_NAME, IDB_DB_VERSION); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(IDB_STORE_NAME)) { + db.createObjectStore(IDB_STORE_NAME); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +// These helpers intentionally throw — callers handle errors so they can log with context. + +async function idbWrite(key: string, value: string): Promise { + const db = await openDB(); + await new Promise((resolve, reject) => { + const tx = db.transaction(IDB_STORE_NAME, 'readwrite'); + tx.objectStore(IDB_STORE_NAME).put(value, key); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} + +async function idbRead(key: string): Promise { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction(IDB_STORE_NAME, 'readonly'); + const request = tx.objectStore(IDB_STORE_NAME).get(key); + request.onsuccess = () => resolve((request.result as string) ?? null); + request.onerror = () => reject(request.error); + }); +} + +async function idbRemove(key: string): Promise { + const db = await openDB(); + await new Promise((resolve, reject) => { + const tx = db.transaction(IDB_STORE_NAME, 'readwrite'); + tx.objectStore(IDB_STORE_NAME).delete(key); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} + +export type StorageWriteResult = { + sessionStorage: boolean; + localStorage: boolean; + indexedDB: boolean; +}; + +/** + * Writes a value to sessionStorage, localStorage, and IndexedDB. + * Failures in any individual layer are logged and swallowed so a single + * unavailable store (e.g. private-browsing restrictions) does not block the flow. + * Returns a result map indicating which layers succeeded. + */ +export async function storageWrite(key: string, value: string): Promise { + const result: StorageWriteResult = { sessionStorage: false, localStorage: false, indexedDB: false }; + + try { + sessionStorage.setItem(key, value); + result.sessionStorage = true; + } catch (err) { + logger.error('oauth2.storage.write_failed', { + storage: { layer: 'sessionStorage', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } + + try { + localStorage.setItem(key, value); + result.localStorage = true; + } catch (err) { + logger.error('oauth2.storage.write_failed', { + storage: { layer: 'localStorage', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } + + try { + await idbWrite(key, value); + result.indexedDB = true; + } catch (err) { + logger.error('oauth2.storage.write_failed', { + storage: { layer: 'indexedDB', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } + + return result; +} + +export type StorageSource = 'sessionStorage' | 'localStorage' | 'indexedDB' | null; + +/** + * Reads a value from sessionStorage first, then localStorage, then IndexedDB. + * Returns the first non-null hit (or null) along with which layer it came from. + */ +export async function storageRead(key: string): Promise<{ value: string | null; source: StorageSource }> { + try { + const fromSession = sessionStorage.getItem(key); + if (fromSession !== null) return { value: fromSession, source: 'sessionStorage' }; + } catch (err) { + logger.error('oauth2.storage.read_failed', { + storage: { layer: 'sessionStorage', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } + + try { + const fromLocal = localStorage.getItem(key); + if (fromLocal !== null) return { value: fromLocal, source: 'localStorage' }; + } catch (err) { + logger.error('oauth2.storage.read_failed', { + storage: { layer: 'localStorage', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } + + try { + const fromIdb = await idbRead(key); + return { value: fromIdb, source: fromIdb !== null ? 'indexedDB' : null }; + } catch (err) { + logger.error('oauth2.storage.read_failed', { + storage: { layer: 'indexedDB', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + return { value: null, source: null }; + } +} + +/** + * Removes a key from sessionStorage, localStorage, and IndexedDB. + */ +export async function storageRemove(key: string): Promise { + try { + sessionStorage.removeItem(key); + } catch (err) { + logger.error('oauth2.storage.remove_failed', { + storage: { layer: 'sessionStorage', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } + + try { + localStorage.removeItem(key); + } catch (err) { + logger.error('oauth2.storage.remove_failed', { + storage: { layer: 'localStorage', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } + + try { + await idbRemove(key); + } catch (err) { + logger.error('oauth2.storage.remove_failed', { + storage: { layer: 'indexedDB', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } +} From 51b87098688a15da540b9eccc027b6a435036183 Mon Sep 17 00:00:00 2001 From: ethella Date: Thu, 5 Mar 2026 14:59:07 -0800 Subject: [PATCH 2/3] Add datadog token --- packages/@magic-ext/oauth2/src/index.ts | 4 +- packages/@magic-ext/oauth2/src/logger.ts | 6 +- packages/@magic-ext/oauth2/src/storage.ts | 72 ++++++++++++++++++++--- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/packages/@magic-ext/oauth2/src/index.ts b/packages/@magic-ext/oauth2/src/index.ts index e1e4de4c0..7dd7e3847 100644 --- a/packages/@magic-ext/oauth2/src/index.ts +++ b/packages/@magic-ext/oauth2/src/index.ts @@ -88,11 +88,11 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { pkce: { provider: configuration.provider, storageLayers: writeResult, - allLayersSucceeded: writeResult.sessionStorage && writeResult.localStorage && writeResult.indexedDB, + allLayersSucceeded: writeResult.sessionStorage && writeResult.localStorage && writeResult.indexedDB && writeResult.cookie, }, }); - if (!writeResult.sessionStorage || !writeResult.localStorage || !writeResult.indexedDB) { + if (!writeResult.sessionStorage || !writeResult.localStorage || !writeResult.indexedDB || !writeResult.cookie) { logger.warn('oauth2.pkce.partial_write', { pkce: { provider: configuration.provider, diff --git a/packages/@magic-ext/oauth2/src/logger.ts b/packages/@magic-ext/oauth2/src/logger.ts index e25ea9fa3..639a5a9ae 100644 --- a/packages/@magic-ext/oauth2/src/logger.ts +++ b/packages/@magic-ext/oauth2/src/logger.ts @@ -8,15 +8,15 @@ * (starts with "pub...") before deploying. */ -// TODO: replace with the real Magic Datadog browser client token -const DATADOG_CLIENT_TOKEN = '__DATADOG_CLIENT_TOKEN__'; +// Temporary Datadog client token for the oauth2 extension. +const DATADOG_CLIENT_TOKEN = 'pub6843da41b336b49cfed0626f60a8ff68'; const DATADOG_INTAKE_URL = 'https://browser-intake-datadoghq.com/api/v2/logs'; const SERVICE = 'magic-oauth2-extension'; export type LogContext = Record; function send(status: 'info' | 'warn' | 'error', message: string, context: LogContext = {}): void { - if (typeof fetch === 'undefined' || !DATADOG_CLIENT_TOKEN || DATADOG_CLIENT_TOKEN === '__DATADOG_CLIENT_TOKEN__') { + if (typeof fetch === 'undefined') { return; } diff --git a/packages/@magic-ext/oauth2/src/storage.ts b/packages/@magic-ext/oauth2/src/storage.ts index abe5f4e2e..884e14df9 100644 --- a/packages/@magic-ext/oauth2/src/storage.ts +++ b/packages/@magic-ext/oauth2/src/storage.ts @@ -4,6 +4,33 @@ const IDB_DB_NAME = 'magic_oauth_db'; const IDB_DB_VERSION = 1; const IDB_STORE_NAME = 'pkce_store'; +// Cookie TTL covers the OAuth round-trip with margin. Short enough to limit exposure. +const COOKIE_MAX_AGE_SECONDS = 600; // 10 minutes + +/** + * Cookies are the most iOS ITP-resilient storage primitive for PKCE data: + * - ITP only restricts *third-party* cookies. A cookie set on the app's own origin + * is first-party and is never touched by ITP. + * - SameSite=Lax allows the cookie to be sent on the top-level GET navigation that + * returns from the OAuth provider — the exact redirect we need to survive. + * - Cookies survive the ASWebAuthenticationSession ↔ WKWebView process boundary, + * whereas sessionStorage, localStorage, and IndexedDB do not cross that boundary. + */ +function cookieWrite(key: string, value: string): void { + const encoded = encodeURIComponent(value); + document.cookie = `${key}=${encoded}; SameSite=Lax; Secure; Path=/; Max-Age=${COOKIE_MAX_AGE_SECONDS}`; +} + +function cookieRead(key: string): string | null { + const prefix = `${key}=`; + const match = document.cookie.split('; ').find((row) => row.startsWith(prefix)); + return match ? decodeURIComponent(match.slice(prefix.length)) : null; +} + +function cookieRemove(key: string): void { + document.cookie = `${key}=; SameSite=Lax; Secure; Path=/; Max-Age=0`; +} + function openDB(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(IDB_DB_NAME, IDB_DB_VERSION); @@ -54,16 +81,17 @@ export type StorageWriteResult = { sessionStorage: boolean; localStorage: boolean; indexedDB: boolean; + cookie: boolean; }; /** - * Writes a value to sessionStorage, localStorage, and IndexedDB. + * Writes a value to sessionStorage, localStorage, IndexedDB, and a first-party cookie. * Failures in any individual layer are logged and swallowed so a single * unavailable store (e.g. private-browsing restrictions) does not block the flow. * Returns a result map indicating which layers succeeded. */ export async function storageWrite(key: string, value: string): Promise { - const result: StorageWriteResult = { sessionStorage: false, localStorage: false, indexedDB: false }; + const result: StorageWriteResult = { sessionStorage: false, localStorage: false, indexedDB: false, cookie: false }; try { sessionStorage.setItem(key, value); @@ -92,14 +120,24 @@ export async function storageWrite(key: string, value: string): Promise { try { @@ -122,17 +160,27 @@ export async function storageRead(key: string): Promise<{ value: string | null; try { const fromIdb = await idbRead(key); - return { value: fromIdb, source: fromIdb !== null ? 'indexedDB' : null }; + if (fromIdb !== null) return { value: fromIdb, source: 'indexedDB' }; } catch (err) { logger.error('oauth2.storage.read_failed', { storage: { layer: 'indexedDB', key, errorMsg: err instanceof Error ? err.message : String(err) }, }); - return { value: null, source: null }; } + + try { + const fromCookie = cookieRead(key); + if (fromCookie !== null) return { value: fromCookie, source: 'cookie' }; + } catch (err) { + logger.error('oauth2.storage.read_failed', { + storage: { layer: 'cookie', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } + + return { value: null, source: null }; } /** - * Removes a key from sessionStorage, localStorage, and IndexedDB. + * Removes a key from sessionStorage, localStorage, IndexedDB, and the cookie. */ export async function storageRemove(key: string): Promise { try { @@ -158,4 +206,12 @@ export async function storageRemove(key: string): Promise { storage: { layer: 'indexedDB', key, errorMsg: err instanceof Error ? err.message : String(err) }, }); } + + try { + cookieRemove(key); + } catch (err) { + logger.error('oauth2.storage.remove_failed', { + storage: { layer: 'cookie', key, errorMsg: err instanceof Error ? err.message : String(err) }, + }); + } } From cbf24fb7fb5142528d203f3d91a6e57efeca4b86 Mon Sep 17 00:00:00 2001 From: ethella Date: Thu, 5 Mar 2026 15:56:43 -0800 Subject: [PATCH 3/3] Add datadog sdk --- packages/@magic-ext/oauth2/package.json | 4 ++ packages/@magic-ext/oauth2/src/index.ts | 15 ++---- packages/@magic-ext/oauth2/src/logger.ts | 59 ++++++++---------------- yarn.lock | 22 +++++++++ 4 files changed, 48 insertions(+), 52 deletions(-) diff --git a/packages/@magic-ext/oauth2/package.json b/packages/@magic-ext/oauth2/package.json index bb3ef64c5..df00f6b8f 100644 --- a/packages/@magic-ext/oauth2/package.json +++ b/packages/@magic-ext/oauth2/package.json @@ -25,9 +25,13 @@ "externals": { "include": [ "@magic-sdk/provider" + ], + "exclude": [ + "@datadog/browser-logs" ] }, "dependencies": { + "@datadog/browser-logs": "^5.0.0", "crypto-js": "^4.2.0" }, "devDependencies": { diff --git a/packages/@magic-ext/oauth2/src/index.ts b/packages/@magic-ext/oauth2/src/index.ts index 7dd7e3847..0fca9fb87 100644 --- a/packages/@magic-ext/oauth2/src/index.ts +++ b/packages/@magic-ext/oauth2/src/index.ts @@ -84,22 +84,13 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { JSON.stringify({ codeVerifier, ...successResult.pkceMetadata }), ); - logger.info('oauth2.pkce.stored', { + const logPayload = { pkce: { provider: configuration.provider, storageLayers: writeResult, - allLayersSucceeded: writeResult.sessionStorage && writeResult.localStorage && writeResult.indexedDB && writeResult.cookie, }, - }); - - if (!writeResult.sessionStorage || !writeResult.localStorage || !writeResult.indexedDB || !writeResult.cookie) { - logger.warn('oauth2.pkce.partial_write', { - pkce: { - provider: configuration.provider, - storageLayers: writeResult, - }, - }); - } + }; + logger.info('oauth2.pkce.stored', logPayload); } if (successResult?.oauthAuthoriationURI) { diff --git a/packages/@magic-ext/oauth2/src/logger.ts b/packages/@magic-ext/oauth2/src/logger.ts index 639a5a9ae..3bfdecffc 100644 --- a/packages/@magic-ext/oauth2/src/logger.ts +++ b/packages/@magic-ext/oauth2/src/logger.ts @@ -1,51 +1,30 @@ /** - * Lightweight Datadog logger for the oauth2 extension. + * Datadog browser-logs logger for the oauth2 extension. * - * Posts directly to the Datadog browser-logs HTTP intake endpoint using fetch — - * no SDK dependency required. The client token is browser-safe (write-only). - * - * Replace DATADOG_CLIENT_TOKEN with the actual Magic Datadog client token - * (starts with "pub...") before deploying. + * Uses the official @datadog/browser-logs SDK, which is bundled into the extension + * output (excluded from externals). The client token is browser-safe (write-only). + * The SDK automatically captures network.client.ip server-side from the request source IP. */ -// Temporary Datadog client token for the oauth2 extension. +import { datadogLogs } from '@datadog/browser-logs'; + const DATADOG_CLIENT_TOKEN = 'pub6843da41b336b49cfed0626f60a8ff68'; -const DATADOG_INTAKE_URL = 'https://browser-intake-datadoghq.com/api/v2/logs'; const SERVICE = 'magic-oauth2-extension'; -export type LogContext = Record; - -function send(status: 'info' | 'warn' | 'error', message: string, context: LogContext = {}): void { - if (typeof fetch === 'undefined') { - return; - } +datadogLogs.init({ + clientToken: DATADOG_CLIENT_TOKEN, + site: 'datadoghq.com', + service: SERVICE, + sessionSampleRate: 100, + forwardErrorsToLogs: false, + usePartitionedCrossSiteSessionCookie: true, + useSecureSessionCookie: true, +}); - const entry = { - ddsource: 'browser', - service: SERVICE, - status, - message, - date: Date.now(), - 'http.useragent': typeof navigator !== 'undefined' ? navigator.userAgent : undefined, - 'page.origin': typeof window !== 'undefined' ? window.location.origin : undefined, - ...context, - }; - - const url = `${DATADOG_INTAKE_URL}?dd-api-key=${DATADOG_CLIENT_TOKEN}&ddsource=browser&service=${SERVICE}`; - - // Fire-and-forget. keepalive ensures the request survives page navigations (e.g. the OAuth redirect). - fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify([entry]), - keepalive: true, - }).catch(() => { - // never let logging errors surface to the caller - }); -} +export type LogContext = Record; export const logger = { - info: (message: string, context?: LogContext) => send('info', message, context), - warn: (message: string, context?: LogContext) => send('warn', message, context), - error: (message: string, context?: LogContext) => send('error', message, context), + info: (message: string, context?: LogContext) => datadogLogs.logger.info(message, context), + warn: (message: string, context?: LogContext) => datadogLogs.logger.warn(message, context), + error: (message: string, context?: LogContext) => datadogLogs.logger.error(message, context), }; diff --git a/yarn.lock b/yarn.lock index a89ab255a..5fce30a9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1604,6 +1604,27 @@ __metadata: languageName: node linkType: hard +"@datadog/browser-core@npm:5.35.1": + version: 5.35.1 + resolution: "@datadog/browser-core@npm:5.35.1" + checksum: f5706a839ed572e59694ac7a13f2d298079f67b9b9fa6ca5e397e1dc72b4c755a90c85d0ed0f4c8f292c38d7e9b8c1e94dbe80dd27fbbd0fe37ecf7aad61a1cb + languageName: node + linkType: hard + +"@datadog/browser-logs@npm:^5.0.0": + version: 5.35.1 + resolution: "@datadog/browser-logs@npm:5.35.1" + dependencies: + "@datadog/browser-core": 5.35.1 + peerDependencies: + "@datadog/browser-rum": 5.35.1 + peerDependenciesMeta: + "@datadog/browser-rum": + optional: true + checksum: 47156dd78f33aa1b6971a8131730bdcd87cd094cfc5ea5bae938107362c86241077a45668f748bb0e70a51d58c1aa4781bbeb4c3b595a04f3ae4a0367da28237 + languageName: node + linkType: hard + "@ecies/ciphers@npm:^0.2.4": version: 0.2.5 resolution: "@ecies/ciphers@npm:0.2.5" @@ -3442,6 +3463,7 @@ __metadata: version: 0.0.0-use.local resolution: "@magic-ext/oauth2@workspace:packages/@magic-ext/oauth2" dependencies: + "@datadog/browser-logs": ^5.0.0 "@magic-sdk/provider": ^33.5.0 "@types/crypto-js": 4.2.0 crypto-js: ^4.2.0