Skip to content
Merged
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
10 changes: 10 additions & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ prod,clsx,MIT,Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)
prod,react,MIT,Copyright (c) Facebook, Inc. and its affiliates.
prod,react-dom,MIT,Copyright (c) Facebook, Inc. and its affiliates.
dev,typedoc,Apache-2.0,TypeStrong
dev,@angular/common,MIT,Copyright (c) 2010-2024 Google LLC
dev,@angular/compiler,MIT,Copyright (c) 2010-2024 Google LLC
dev,@angular/compiler-cli,MIT,Copyright (c) 2010-2024 Google LLC
dev,@angular/core,MIT,Copyright (c) 2010-2024 Google LLC
dev,@angular/platform-browser,MIT,Copyright (c) 2010-2024 Google LLC
dev,@angular/router,MIT,Copyright (c) 2010-2024 Google LLC
dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
dev,@jsdevtools/coverage-istanbul-loader,MIT,Copyright (c) 2015 James Messinger
dev,@ngtools/webpack,MIT,Copyright (c) 2017 Google LLC
dev,@playwright/test,Apache-2.0,Copyright Microsoft Corporation
dev,@swc/core,Apache-2.0,Copyright (c) SWC Contributors
dev,@types/chrome,MIT,Copyright Microsoft Corporation
Expand All @@ -30,6 +37,7 @@ dev,@vitejs/plugin-react,MIT,Copyright (c) 2019-present Evan You & Vite Contribu
dev,@module-federation/enhanced,MIT, Copyright (c) 2020 ScriptedAlchemy LLC (Zack Jackson) Zhou Shaw (zhouxiao)
dev,@vue/test-utils,MIT,Copyright (c) 2021-present vuejs
dev,ajv,MIT,Copyright 2015-2017 Evgeny Poberezkin
dev,babel-loader,MIT,Copyright (c) 2014-2019 Luís Couto <hello@luiscouto.pt>
dev,browserstack-local,MIT,Copyright 2016 BrowserStack
dev,chrome-webstore-upload,MIT,Copyright Federico Brigante <me@fregante.com> (https://fregante.com), 2020 Andrew Levine
dev,busboy,MIT,Copyright Brian White
Expand Down Expand Up @@ -68,6 +76,7 @@ dev,react-router,MIT,Copyright (c) React Training LLC 2015-2019 Copyright (c) Re
dev,react-router-dom,MIT,Copyright (c) React Training LLC 2015-2019 Copyright (c) Remix Software Inc. 2020-2021 Copyright (c) Shopify Inc. 2022-2023
dev,react-window,MIT,Copyright (c) 2018 Brian Vaughn
dev,recharts,MIT,Copyright (c) 2015-present recharts
dev,rxjs,Apache-2.0,Copyright (c) 2015-2022 Google LLC Ben Lesh and contributors
dev,swc-loader,MIT,Copyright (c) SWC Contributors
dev,terser-webpack-plugin,MIT,Copyright JS Foundation and other contributors
dev,ts-loader,MIT,Copyright 2015 TypeStrong
Expand All @@ -82,3 +91,4 @@ dev,webpack,MIT,Copyright JS Foundation and other contributors
dev,webpack-cli,MIT,Copyright JS Foundation and other contributors
dev,webpack-dev-middleware,MIT,Copyright JS Foundation and other contributors
dev,wxt,MIT,Copyright (c) 2023 Aaron
dev,zone.js,MIT,Copyright (c) 2010-2024 Google LLC
3 changes: 3 additions & 0 deletions eslint-local-rules/disallowSideEffects.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const packagesWithoutSideEffect = new Set([
'react-router-dom',
'vue',
'vue-router',
'@angular/core',
'@angular/router',
'rxjs',
])

/**
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default tseslint.config(
'test/apps/react-shopist-like',
'test/apps/microfrontend',
'test/apps/nextjs',
'test/apps/angular-app',
'sandbox',
'coverage',
'rum-events-format',
Expand Down
6 changes: 6 additions & 0 deletions packages/rum-angular/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*
!/cjs/**/*
!/esm/**/*
!/src/**/*
/src/**/*.spec.ts
/src/**/*.specHelper.ts
39 changes: 39 additions & 0 deletions packages/rum-angular/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@datadog/browser-rum-angular",
"private": true,
"license": "Apache-2.0",
"main": "cjs/entries/main.js",
"module": "esm/entries/main.js",
"types": "cjs/entries/main.d.ts",
"scripts": {
"build": "node ../../scripts/build/build-package.ts --modules",
"prepack": "npm run build"
},
"dependencies": {
"@datadog/browser-core": "6.31.0",
"@datadog/browser-rum-core": "6.31.0"
},
"peerDependencies": {
"@angular/core": ">=15 <=21",
"@angular/router": ">=15 <=21",
"rxjs": ">=7"
},
"repository": {
"type": "git",
"url": "https://github.com/DataDog/browser-sdk.git",
"directory": "packages/rum-angular"
},
"volta": {
"extends": "../../package.json"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@angular/common": "19.2.5",
"@angular/compiler": "19.2.5",
"@angular/core": "19.2.5",
"@angular/router": "19.2.5",
"rxjs": "7.8.2"
}
Comment thread
rgaignault marked this conversation as resolved.
}
98 changes: 98 additions & 0 deletions packages/rum-angular/src/domain/angularPlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core'
import { registerCleanupTask } from '../../../core/test'
import { angularPlugin, onRumInit, onRumStart, resetAngularPlugin } from './angularPlugin'

const PUBLIC_API = {} as RumPublicApi
const INIT_CONFIGURATION = {} as RumInitConfiguration

describe('angularPlugin', () => {
beforeEach(() => {
registerCleanupTask(() => {
resetAngularPlugin()
})
})

it('returns a plugin object', () => {
const plugin = angularPlugin()
expect(plugin).toEqual(
jasmine.objectContaining({
name: 'angular',
onInit: jasmine.any(Function),
onRumStart: jasmine.any(Function),
})
)
})

it('calls callbacks registered with onRumInit during onInit', () => {
const callbackSpy = jasmine.createSpy()
const pluginConfiguration = {}
onRumInit(callbackSpy)

expect(callbackSpy).not.toHaveBeenCalled()

angularPlugin(pluginConfiguration).onInit!({
publicApi: PUBLIC_API,
initConfiguration: INIT_CONFIGURATION,
})

expect(callbackSpy).toHaveBeenCalledTimes(1)
expect(callbackSpy.calls.mostRecent().args[0]).toBe(pluginConfiguration)
expect(callbackSpy.calls.mostRecent().args[1]).toBe(PUBLIC_API)
})

it('calls callbacks immediately if onInit was already invoked', () => {
const callbackSpy = jasmine.createSpy()
const pluginConfiguration = {}
angularPlugin(pluginConfiguration).onInit!({
publicApi: PUBLIC_API,
initConfiguration: INIT_CONFIGURATION,
})

onRumInit(callbackSpy)

expect(callbackSpy).toHaveBeenCalledTimes(1)
expect(callbackSpy.calls.mostRecent().args[0]).toBe(pluginConfiguration)
expect(callbackSpy.calls.mostRecent().args[1]).toBe(PUBLIC_API)
})

it('enforce manual view tracking when router is enabled', () => {
const initConfiguration = { ...INIT_CONFIGURATION }
angularPlugin({ router: true }).onInit!({ publicApi: PUBLIC_API, initConfiguration })

expect(initConfiguration.trackViewsManually).toBe(true)
})

it('does not enforce manual view tracking when router is disabled', () => {
const initConfiguration = { ...INIT_CONFIGURATION }
angularPlugin({ router: false }).onInit!({ publicApi: PUBLIC_API, initConfiguration })

expect(initConfiguration.trackViewsManually).toBeUndefined()
})

it('returns the configuration telemetry', () => {
const pluginConfiguration = { router: true }
const plugin = angularPlugin(pluginConfiguration)

expect(plugin.getConfigurationTelemetry!()).toEqual({ router: true })
})

it('calls onRumStart subscribers during onRumStart', () => {
const callbackSpy = jasmine.createSpy()
const addErrorSpy = jasmine.createSpy()
onRumStart(callbackSpy)

angularPlugin().onRumStart!({ addError: addErrorSpy })

expect(callbackSpy).toHaveBeenCalledWith(addErrorSpy)
})

it('calls onRumStart subscribers immediately if already started', () => {
const addErrorSpy = jasmine.createSpy()
angularPlugin().onRumStart!({ addError: addErrorSpy })

const callbackSpy = jasmine.createSpy()
onRumStart(callbackSpy)

expect(callbackSpy).toHaveBeenCalledWith(addErrorSpy)
})
})
93 changes: 93 additions & 0 deletions packages/rum-angular/src/domain/angularPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { RumPlugin, RumPublicApi, StartRumResult } from '@datadog/browser-rum-core'

type InitSubscriber = (configuration: AngularPluginConfiguration, rumPublicApi: RumPublicApi) => void
type StartSubscriber = (addError: StartRumResult['addError']) => void

let globalPublicApi: RumPublicApi | undefined
let globalConfiguration: AngularPluginConfiguration | undefined
let globalAddError: StartRumResult['addError'] | undefined

const onRumInitSubscribers: InitSubscriber[] = []
const onRumStartSubscribers: StartSubscriber[] = []

/**
* Angular plugin configuration.
*
* @category Main
*/
export interface AngularPluginConfiguration {
/**
* Enable Angular Router integration. Make sure to use `provideDatadogRouter()` in your
* application providers.
*/
router?: boolean
}

/**
* Angular plugin constructor.
*
* @category Main
* @example
* ```ts
* import { datadogRum } from '@datadog/browser-rum'
* import { angularPlugin } from '@datadog/browser-rum-angular'
*
* datadogRum.init({
* applicationId: '<DATADOG_APPLICATION_ID>',
* clientToken: '<DATADOG_CLIENT_TOKEN>',
* site: '<DATADOG_SITE>',
* plugins: [angularPlugin({ router: true })],
* // ...
* })
* ```
*/
export function angularPlugin(configuration: AngularPluginConfiguration = {}): RumPlugin {
return {
name: 'angular',
onInit({ publicApi, initConfiguration }) {
globalPublicApi = publicApi
globalConfiguration = configuration
for (const subscriber of onRumInitSubscribers) {
subscriber(globalConfiguration, globalPublicApi)
}
if (configuration.router) {
initConfiguration.trackViewsManually = true
}
},
onRumStart({ addError }) {
globalAddError = addError
if (addError) {
for (const subscriber of onRumStartSubscribers) {
subscriber(addError)
}
}
},
getConfigurationTelemetry() {
return { router: !!configuration.router }
},
} satisfies RumPlugin
}

export function onRumInit(callback: InitSubscriber) {
if (globalConfiguration && globalPublicApi) {
callback(globalConfiguration, globalPublicApi)
} else {
onRumInitSubscribers.push(callback)
}
}

export function onRumStart(callback: StartSubscriber) {
if (globalAddError) {
callback(globalAddError)
} else {
onRumStartSubscribers.push(callback)
}
}

export function resetAngularPlugin() {
globalPublicApi = undefined
globalConfiguration = undefined
globalAddError = undefined
onRumInitSubscribers.length = 0
onRumStartSubscribers.length = 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { EnvironmentProviders } from '@angular/core'
import { ENVIRONMENT_INITIALIZER, inject, makeEnvironmentProviders } from '@angular/core'
import { GuardsCheckEnd, Router } from '@angular/router'
import { filter } from 'rxjs'
import { startAngularView } from './startAngularView'

/**
* Angular provider that subscribes to Router events and starts a new RUM view
* on each GuardsCheckEnd, using the matched route template as the view name.
*
* GuardsCheckEnd fires after guards pass but before resolvers run, so data
* fetches from resolvers are correctly attributed to the new view.
*
* @category Main
* @example
* ```ts
* import { bootstrapApplication } from '@angular/platform-browser'
* import { provideRouter } from '@angular/router'
* import { provideDatadogRouter } from '@datadog/browser-rum-angular'
*
* bootstrapApplication(AppComponent, {
* providers: [
* provideRouter(routes),
* provideDatadogRouter(),
* ],
* })
* ```
*/
export function provideDatadogRouter(): EnvironmentProviders {
return makeEnvironmentProviders([
{
// Needed for Angular v15 support (provideEnvironmentInitializer requires v16+)
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory: () => {
const router = inject(Router)

return () => {
// No unsubscribe needed as its for the full app lifecycle and because DestroyRef requires v16+
Comment thread
amortemousque marked this conversation as resolved.
router.events
.pipe(filter((event): event is GuardsCheckEnd => event instanceof GuardsCheckEnd))
.subscribe((event) => {
const root = event.state.root
startAngularView(root, event.urlAfterRedirects)
})
}
},
},
])
}
Loading
Loading