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
3 changes: 2 additions & 1 deletion packages/core/src/tools/stackTrace/handlingStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ export function createHandlingStack(
| 'instrumented method'
| 'log'
| 'react error'
| 'nextjs error'
| 'vue error'
| 'view'
| 'vital'
| 'nextjs error'
): string {
/**
* Skip the two internal frames:
Expand Down
45 changes: 45 additions & 0 deletions packages/rum-vue/src/domain/error/addVueError.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { initializeVuePlugin } from '../../../test/initializeVuePlugin'
import { addVueError } from './addVueError'

describe('addVueError', () => {
it('reports the error to the SDK', () => {
const addErrorSpy = jasmine.createSpy()
initializeVuePlugin({ addError: addErrorSpy })

const error = new Error('something broke')
addVueError(error, null, 'mounted hook')

expect(addErrorSpy).toHaveBeenCalledOnceWith(
jasmine.objectContaining({
error,
handlingStack: jasmine.any(String),
componentStack: 'mounted hook',
startClocks: jasmine.any(Object),
context: { framework: 'vue' },
})
)
})

it('handles empty info gracefully', () => {
const addErrorSpy = jasmine.createSpy()
initializeVuePlugin({ addError: addErrorSpy })
addVueError(new Error('oops'), null, '')
expect(addErrorSpy).toHaveBeenCalledTimes(1)
expect(addErrorSpy.calls.mostRecent().args[0].componentStack).toBeUndefined()
})

it('should merge dd_context from the original error with vue error context', () => {
const addErrorSpy = jasmine.createSpy()
initializeVuePlugin({ addError: addErrorSpy })
const originalError = new Error('error message')
;(originalError as any).dd_context = { component: 'Menu', param: 123 }

addVueError(originalError, null, 'mounted hook')

expect(addErrorSpy.calls.mostRecent().args[0].context).toEqual({
framework: 'vue',
component: 'Menu',
param: 123,
})
})
})
41 changes: 41 additions & 0 deletions packages/rum-vue/src/domain/error/addVueError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ComponentPublicInstance } from 'vue'
import { callMonitored, clocksNow, createHandlingStack } from '@datadog/browser-core'
import { onVueStart } from '../vuePlugin'

/**
* Add a Vue error to the RUM session.
*
* @category Error
* @example
* ```ts
* import { createApp } from 'vue'
* import { addVueError } from '@datadog/browser-rum-vue'
*
* const app = createApp(App)
* // Report all Vue errors to Datadog automatically
* app.config.errorHandler = addVueError
* ```
*/
export function addVueError(
error: unknown,
// Required by Vue's app.config.errorHandler signature, but not used by the SDK
_instance: ComponentPublicInstance | null,
info: string
) {
const handlingStack = createHandlingStack('vue error')
const startClocks = clocksNow()
onVueStart((addError) => {
callMonitored(() => {
addError({
error,
handlingStack,
componentStack: info || undefined,
startClocks,
context: {
...(typeof error === 'object' && error !== null ? (error as { dd_context?: object }).dd_context : undefined),
framework: 'vue',
},
})
})
})
}
48 changes: 48 additions & 0 deletions packages/rum-vue/src/domain/vuePlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core'
import { registerCleanupTask } from '../../../core/test'
import { onVueInit, vuePlugin, resetVuePlugin } from './vuePlugin'

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

describe('vuePlugin', () => {
beforeEach(() => {
registerCleanupTask(() => resetVuePlugin())
})

it('returns a plugin object with name "vue"', () => {
expect(vuePlugin()).toEqual(jasmine.objectContaining({ name: 'vue' }))
})

it('calls callbacks registered with onVueInit during onInit', () => {
const spy = jasmine.createSpy()
const config = {}
onVueInit(spy)
vuePlugin(config).onInit({ publicApi: PUBLIC_API, initConfiguration: INIT_CONFIGURATION })
expect(spy).toHaveBeenCalledOnceWith(config, PUBLIC_API)
})

it('calls callbacks immediately if onInit was already invoked', () => {
const spy = jasmine.createSpy()
const config = {}
vuePlugin(config).onInit({ publicApi: PUBLIC_API, initConfiguration: INIT_CONFIGURATION })
onVueInit(spy)
expect(spy).toHaveBeenCalledOnceWith(config, PUBLIC_API)
})

it('sets trackViewsManually when router is true', () => {
const initConfiguration = { ...INIT_CONFIGURATION }
vuePlugin({ router: true }).onInit({ publicApi: PUBLIC_API, initConfiguration })
expect(initConfiguration.trackViewsManually).toBe(true)
})

it('does not set trackViewsManually when router is false', () => {
const initConfiguration = { ...INIT_CONFIGURATION }
vuePlugin({ router: false }).onInit({ publicApi: PUBLIC_API, initConfiguration })
expect(initConfiguration.trackViewsManually).toBeUndefined()
})

it('returns configuration telemetry', () => {
expect(vuePlugin({ router: true }).getConfigurationTelemetry()).toEqual({ router: true })
})
})
68 changes: 68 additions & 0 deletions packages/rum-vue/src/domain/vuePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { RumPlugin, RumPublicApi, StartRumResult } from '@datadog/browser-rum-core'

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

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

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

export interface VuePluginConfiguration {
router?: boolean
}

export type VuePlugin = Required<RumPlugin>

export function vuePlugin(configuration: VuePluginConfiguration = {}): VuePlugin {
return {
name: 'vue',
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 onVueInit(callback: InitSubscriber) {
if (globalConfiguration && globalPublicApi) {
callback(globalConfiguration, globalPublicApi)
} else {
onRumInitSubscribers.push(callback)
}
}

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

export function resetVuePlugin() {
globalPublicApi = undefined
globalConfiguration = undefined
globalAddError = undefined
onRumInitSubscribers.length = 0
onRumStartSubscribers.length = 0
}
4 changes: 3 additions & 1 deletion packages/rum-vue/src/entries/main.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export {}
export type { VuePluginConfiguration, VuePlugin } from '../domain/vuePlugin'
export { vuePlugin } from '../domain/vuePlugin'
export { addVueError } from '../domain/error/addVueError'
26 changes: 26 additions & 0 deletions packages/rum-vue/test/initializeVuePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { RumInitConfiguration, RumPublicApi, StartRumResult } from '@datadog/browser-rum-core'
import { noop } from '@datadog/browser-core'
import type { VuePluginConfiguration } from '../src/domain/vuePlugin'
import { vuePlugin, resetVuePlugin } from '../src/domain/vuePlugin'
import { registerCleanupTask } from '../../core/test'

export function initializeVuePlugin({
configuration = {},
initConfiguration = {},
publicApi = {},
addError = noop,
}: {
configuration?: VuePluginConfiguration
initConfiguration?: Partial<RumInitConfiguration>
publicApi?: Partial<RumPublicApi>
addError?: StartRumResult['addError']
} = {}) {
resetVuePlugin()
const plugin = vuePlugin(configuration)
plugin.onInit({
publicApi: publicApi as RumPublicApi,
initConfiguration: initConfiguration as RumInitConfiguration,
})
plugin.onRumStart({ addError })
registerCleanupTask(() => resetVuePlugin())
}
Loading