diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 11e6a6b46..594212a48 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -74,7 +74,18 @@ export type BundlerReport = { version: string; }; -export type ToInjectItem = { type: 'file' | 'code'; value: string; fallback?: ToInjectItem }; +export type InjectedValue = string | (() => Promise); +export enum InjectPosition { + BEFORE, + MIDDLE, + AFTER, +} +export type ToInjectItem = { + type: 'file' | 'code'; + value: InjectedValue; + position?: InjectPosition; + fallback?: ToInjectItem; +}; export type GetLogger = (name: string) => Logger; export type Logger = { @@ -133,7 +144,7 @@ export interface Options extends BaseOptions { customPlugins?: GetCustomPlugins; } -export type GetPluginsOptions = Required; +export type GetPluginsOptions = BaseOptions; export type OptionsWithDefaults = Assign; export type PluginName = `datadog-${Lowercase}-plugin`; diff --git a/packages/plugins/rum/src/index.ts b/packages/plugins/rum/src/index.ts index 6cb5ddd7b..efe44f108 100644 --- a/packages/plugins/rum/src/index.ts +++ b/packages/plugins/rum/src/index.ts @@ -22,7 +22,37 @@ export const getPlugins: GetPlugins = ( log: Logger, ) => { // Verify configuration. +<<<<<<< HEAD const rumOptions = validateOptions(opts, log); +======= + const options = validateOptions(opts, log); + + if (options.sdk) { + // Inject the SDK from the CDN. + context.inject({ + type: 'file', + position: InjectPosition.BEFORE, + value: 'https://www.datadoghq-browser-agent.com/us1/v5/datadog-rum.js', + }); + + if (options.react) { + // Inject the rum-react-plugin. + // NOTE: These files are built from "@dd/tools/rollupConfig.mjs" and available in the distributed package. + context.inject({ + type: 'file', + position: InjectPosition.MIDDLE, + value: path.join(__dirname, './rum-react-plugin.js'), + }); + } + + context.inject({ + type: 'code', + position: InjectPosition.MIDDLE, + value: getInjectionValue(options as RumOptionsWithSdk, context), + }); + } + +>>>>>>> 2aae126 (createBrowserRouter auto instrumentation) return [ { name: 'datadog-rum-sourcemaps-plugin', @@ -37,6 +67,39 @@ export const getPlugins: GetPlugins = ( await uploadSourcemaps(rumOptions as RumOptionsWithSourcemaps, context, log); } }, + transform(code) { + let updatedCode = code; + const createBrowserRouterImportRegExp = new RegExp( + /(import \{.*)createBrowserRouter[,]?(.*\} from "react-router-dom")/g, + ); + const hasCreateBrowserRouterImport = + code.match(createBrowserRouterImportRegExp) !== null; + + if (hasCreateBrowserRouterImport) { + // Remove the import of createBrowserRouter + updatedCode = updatedCode.replace( + createBrowserRouterImportRegExp, + (_, p1, p2) => { + return `${p1}${p2}`; + }, + ); + + // replace all occurences of `createBrowserRouter` with `DD_RUM.createBrowserRouter` + updatedCode = updatedCode.replace( + new RegExp(/createBrowserRouter/g), + 'DD_RUM.createBrowserRouter', + ); + } + + return updatedCode; + }, + transformInclude(id) { + return ( + // @ts-ignore + options?.react?.router === true && + id.match(new RegExp(/.*\.(js|jsx|ts|tsx)$/)) !== null + ); + }, }, ]; }; diff --git a/packages/plugins/rum/src/sdk.ts b/packages/plugins/rum/src/sdk.ts new file mode 100644 index 000000000..5f6b64837 --- /dev/null +++ b/packages/plugins/rum/src/sdk.ts @@ -0,0 +1,77 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +import { doRequest } from '@dd/core/helpers'; +import type { GlobalContext, InjectedValue } from '@dd/core/types'; + +import type { RumOptionsWithDefaults, RumOptionsWithSdk } from './types'; + +type RumAppResponse = { + data: { + attributes: { + client_token: string; + }; + }; +}; + +const getContent = (opts: RumOptionsWithDefaults) => { + const pluginContent = opts.react + ? // @ts-ignore + `, plugins: [reactPlugin({router: ${opts.react.router}})]` + : ''; + const sessionReplayStartCommand = opts.startSessionReplayRecording + ? 'DD_RUM.startSessionReplayRecording();\n' + : ''; + + return `DD_RUM.init({${JSON.stringify(opts.sdk).replace(/(^{|}$)/g, '')}${pluginContent}});${sessionReplayStartCommand} + `; +}; + +export const getInjectionValue = ( + options: RumOptionsWithSdk, + context: GlobalContext, +): InjectedValue => { + const sdkOpts = options.sdk; + // We already have the clientToken, we can inject it directly. + if (sdkOpts.clientToken) { + return getContent(options); + } + + // Let's fetch the clientToken from the API. + if (!context.auth?.apiKey || !context.auth?.appKey) { + throw new Error('Missing auth.apiKey and/or auth.appKey to fetch clientToken.'); + } + + let clientToken: string; + + return async () => { + try { + // Fetch the client token from the API. + const appResponse = await doRequest({ + url: `https://api.datadoghq.com/api/v2/rum/applications/${sdkOpts.applicationId}`, + type: 'json', + auth: context.auth, + }); + + clientToken = appResponse.data?.attributes?.client_token; + } catch (e: any) { + // Could not fetch the clientToken. + // Let's crash the build. + throw new Error(`Could not fetch the clientToken: ${e.message}`); + } + + // Still no clientToken. + if (!clientToken) { + throw new Error('Missing clientToken in the API response.'); + } + + return getContent({ + ...options, + sdk: { + clientToken, + ...sdkOpts, + }, + }); + }; +}; diff --git a/packages/plugins/rum/src/types.ts b/packages/plugins/rum/src/types.ts index ac70fac15..832bbf143 100644 --- a/packages/plugins/rum/src/types.ts +++ b/packages/plugins/rum/src/types.ts @@ -20,14 +20,26 @@ export type RumSourcemapsOptions = { export type RumOptions = { disabled?: boolean; +<<<<<<< HEAD sourcemaps?: RumSourcemapsOptions; +======= + sdk?: SDKOptions; + react?: ReactOptions; + startSessionReplayRecording?: boolean; +>>>>>>> 2aae126 (createBrowserRouter auto instrumentation) }; export type RumSourcemapsOptionsWithDefaults = Required; export type RumOptionsWithDefaults = { disabled?: boolean; +<<<<<<< HEAD sourcemaps?: RumSourcemapsOptionsWithDefaults; +======= + sdk?: SDKOptionsWithDefaults; + react?: ReactOptionsWithDefaults; + startSessionReplayRecording?: boolean; +>>>>>>> 2aae126 (createBrowserRouter auto instrumentation) }; export type RumOptionsWithSourcemaps = { diff --git a/packages/tools/src/build/rumReactPlugin.ts b/packages/tools/src/build/rumReactPlugin.ts new file mode 100644 index 000000000..a7ea31f0b --- /dev/null +++ b/packages/tools/src/build/rumReactPlugin.ts @@ -0,0 +1,9 @@ +import { createBrowserRouter } from '@datadog/browser-rum-react/react-router-v6'; +import { reactPlugin } from '@datadog/browser-rum-react'; + +(() => { + const globalAny: any = global; + globalAny.DD_RUM = globalAny.DD_RUM || {}; + globalAny.reactPlugin = reactPlugin; + globalAny.DD_RUM.createBrowserRouter = createBrowserRouter; +})(); diff --git a/packages/tools/src/rollupConfig.mjs b/packages/tools/src/rollupConfig.mjs index bf6d25d46..59968deec 100644 --- a/packages/tools/src/rollupConfig.mjs +++ b/packages/tools/src/rollupConfig.mjs @@ -66,6 +66,10 @@ export const getDefaultBuildConfigs = (packageJson) => [ format: 'cjs', }, }), +<<<<<<< HEAD +======= + // Type definitions. +>>>>>>> 2aae126 (createBrowserRouter auto instrumentation) // FIXME: This build is sloooow. bundle(packageJson, { plugins: [dts()],