diff --git a/packages/platforms/browser/lib/auto-instrumentation/full-page-load-plugin.ts b/packages/platforms/browser/lib/auto-instrumentation/full-page-load-plugin.ts index caca476b9..797c6fe19 100644 --- a/packages/platforms/browser/lib/auto-instrumentation/full-page-load-plugin.ts +++ b/packages/platforms/browser/lib/auto-instrumentation/full-page-load-plugin.ts @@ -6,6 +6,7 @@ import { getPermittedAttributes } from '../send-page-attributes' import type { WebVitals } from '../web-vitals' import { instrumentPageLoadPhaseSpans } from './page-load-phase-spans' import { defaultRouteResolver } from '../default-routing-provider' +import type { AppState } from '../../../../core/lib/core' export class FullPageLoadPlugin implements Plugin { private readonly spanFactory: SpanFactory @@ -14,6 +15,7 @@ export class FullPageLoadPlugin implements Plugin { private readonly onSettle: OnSettle private readonly webVitals: WebVitals private readonly performance: PerformanceWithTiming + private readonly setAppState: (appState: AppState) => void // if the page was backgrounded at any point in the loading process a page // load span is invalidated as the browser will deprioritise the page @@ -26,7 +28,8 @@ export class FullPageLoadPlugin implements Plugin { webVitals: WebVitals, onSettle: OnSettle, backgroundingListener: BackgroundingListener, - performance: PerformanceWithTiming + performance: PerformanceWithTiming, + setAppState: (appState: AppState) => void ) { this.document = document this.location = location @@ -34,6 +37,7 @@ export class FullPageLoadPlugin implements Plugin { this.webVitals = webVitals this.onSettle = onSettle this.performance = performance + this.setAppState = setAppState backgroundingListener.onStateChange(state => { if (!this.wasBackgrounded && state === 'in-background') { @@ -87,6 +91,7 @@ export class FullPageLoadPlugin implements Plugin { this.webVitals.attachTo(span) this.spanFactory.endSpan(span, endTime) + this.setAppState("ready") }) } } diff --git a/packages/platforms/browser/lib/auto-instrumentation/route-change-plugin.ts b/packages/platforms/browser/lib/auto-instrumentation/route-change-plugin.ts index 8491dc8d3..ea12460c6 100644 --- a/packages/platforms/browser/lib/auto-instrumentation/route-change-plugin.ts +++ b/packages/platforms/browser/lib/auto-instrumentation/route-change-plugin.ts @@ -4,6 +4,7 @@ import type { BrowserConfiguration } from '../config' import type { RouteChangeSpanEndOptions, RouteChangeSpanOptions } from '../routing-provider' import { getPermittedAttributes } from '../send-page-attributes' import { defaultRouteResolver } from '../default-routing-provider' +import type { AppState } from '../../../../core/lib/core' // exclude isFirstClass from the route change option schema const { startTime, parentContext, makeCurrentContext } = coreSpanOptionSchema @@ -27,7 +28,8 @@ export class RouteChangePlugin implements Plugin { constructor ( private readonly spanFactory: SpanFactory, private readonly location: Location, - private readonly document: Document + private readonly document: Document, + private readonly setAppState: (appState: AppState) => void ) {} configure (configuration: InternalConfiguration) { @@ -117,6 +119,7 @@ export class RouteChangePlugin implements Plugin { span.name = `[RouteChange]${route}` span.setAttribute('bugsnag.browser.page.route', route) previousRoute = route + this.setAppState('navigating') // update the URL attribute as well if (permittedAttributes.url) { @@ -125,6 +128,7 @@ export class RouteChangePlugin implements Plugin { } this.spanFactory.toPublicApi(span).end(options.endTime) + this.setAppState('ready') } } satisfies Span diff --git a/packages/platforms/browser/lib/browser.ts b/packages/platforms/browser/lib/browser.ts index b688936ea..3d2438a09 100644 --- a/packages/platforms/browser/lib/browser.ts +++ b/packages/platforms/browser/lib/browser.ts @@ -15,10 +15,12 @@ import makeBrowserPersistence from './persistence' import createResourceAttributesSource from './resource-attributes-source' import createSpanAttributesSource from './span-attributes-source' import { WebVitals } from './web-vitals' +import type { AppState } from '../../../core/lib/core' export let onSettle: OnSettlePlugin export let DefaultRoutingProvider: ReturnType let BugsnagPerformance: Client +let setAppState: (appState: AppState) => void; if (typeof window === 'undefined' || typeof document === 'undefined') { onSettle = createNoopOnSettle() @@ -40,7 +42,8 @@ if (typeof window === 'undefined' || typeof document === 'undefined') { xhrRequestTracker, performance ) - DefaultRoutingProvider = createDefaultRoutingProvider(onSettle, window.location) + + DefaultRoutingProvider = createDefaultRoutingProvider(onSettle, window.location, setAppState) BugsnagPerformance = createClient({ backgroundingListener, @@ -50,7 +53,9 @@ if (typeof window === 'undefined' || typeof document === 'undefined') { deliveryFactory: createFetchDeliveryFactory(window.fetch, clock, backgroundingListener), idGenerator, schema: createSchema(window.location.hostname, new DefaultRoutingProvider()), - plugins: (spanFactory, spanContextStorage) => [ + plugins: (spanFactory, spanContextStorage, setAppState) => { + DefaultRoutingProvider.setAppState = setAppState('starting'); + return [ onSettle, new FullPageLoadPlugin( document, @@ -59,14 +64,15 @@ if (typeof window === 'undefined' || typeof document === 'undefined') { webVitals, onSettle, backgroundingListener, - performance + performance, + setAppState ), // ResourceLoadPlugin should always come after FullPageLoad plugin, as it should use that // span context as the parent of it's spans new ResourceLoadPlugin(spanFactory, spanContextStorage, window.PerformanceObserver), new NetworkRequestPlugin(spanFactory, spanContextStorage, fetchRequestTracker, xhrRequestTracker), - new RouteChangePlugin(spanFactory, window.location, document) - ], + new RouteChangePlugin(spanFactory, window.location, document, setAppState) + ]}, persistence, retryQueueFactory: (delivery, retryQueueMaxSize) => new InMemoryQueue(delivery, retryQueueMaxSize) }) diff --git a/packages/platforms/browser/lib/default-routing-provider.ts b/packages/platforms/browser/lib/default-routing-provider.ts index b33788ba5..4d11d02df 100644 --- a/packages/platforms/browser/lib/default-routing-provider.ts +++ b/packages/platforms/browser/lib/default-routing-provider.ts @@ -1,6 +1,7 @@ import type { OnSettle } from './on-settle' import { getAbsoluteUrl } from '@bugsnag/request-tracker-performance' import type { RouteResolver, RoutingProvider, StartRouteChangeCallback } from './routing-provider' +import { AppState } from '@bugsnag/core-performance/dist/types/core' export const defaultRouteResolver: RouteResolver = (url: URL) => url.pathname || '/' @@ -16,21 +17,25 @@ export const createNoopRoutingProvider = () => { } } -export const createDefaultRoutingProvider = (onSettle: OnSettle, location: Location) => { +export const createDefaultRoutingProvider = (onSettle: OnSettle, location: Location, setAppState?: (appState: AppState) => void) => { return class DefaultRoutingProvider implements RoutingProvider { resolveRoute: RouteResolver - + setAppState?: (appState: AppState) => void + constructor (resolveRoute = defaultRouteResolver) { this.resolveRoute = resolveRoute + this.setAppState = setAppState } listenForRouteChanges (startRouteChangeSpan: StartRouteChangeCallback) { addEventListener('popstate', (ev) => { const url = new URL(location.href) const span = startRouteChangeSpan(url, 'popstate') + this.setAppState?.('navigating') onSettle((endTime) => { span.end(endTime) + this.setAppState?.('ready') }) }) @@ -41,9 +46,11 @@ export const createDefaultRoutingProvider = (onSettle: OnSettle, location: Locat if (url) { const absoluteURL = new URL(getAbsoluteUrl(url.toString(), document.baseURI)) const span = startRouteChangeSpan(absoluteURL, 'pushState') + // this.setAppState?.('navigating') onSettle((endTime) => { span.end(endTime) + // this.setAppState?.('ready') }) } diff --git a/packages/platforms/react-native/lib/auto-instrumentation/app-start-plugin.tsx b/packages/platforms/react-native/lib/auto-instrumentation/app-start-plugin.tsx index 09c448e16..9612e95c9 100644 --- a/packages/platforms/react-native/lib/auto-instrumentation/app-start-plugin.tsx +++ b/packages/platforms/react-native/lib/auto-instrumentation/app-start-plugin.tsx @@ -9,6 +9,7 @@ import React from 'react' import type { AppRegistry, WrapperComponentProvider } from 'react-native' import type { ReactNativeConfiguration } from '../config' import { createAppStartSpan } from '../create-app-start-span' +import type { AppState } from '../../../../core/lib/core' interface WrapperProps { children: ReactNode @@ -22,17 +23,20 @@ export class AppStartPlugin implements Plugin { private readonly spanFactory: SpanFactory private readonly clock: Clock private readonly appRegistry: typeof AppRegistry + private readonly setAppState: (appState: AppState) => void constructor ( appStartTime: number, spanFactory: SpanFactory, clock: Clock, - appRegistry: typeof AppRegistry + appRegistry: typeof AppRegistry, + setAppState: (appState: AppState) => void ) { this.appStartTime = appStartTime this.spanFactory = spanFactory this.clock = clock this.appRegistry = appRegistry + this.setAppState = setAppState } configure (configuration: InternalConfiguration) { @@ -44,6 +48,7 @@ export class AppStartPlugin implements Plugin { React.useEffect(() => { if (appStartSpan.isValid()) { this.spanFactory.endSpan(appStartSpan, this.clock.now()) + this.setAppState('ready') } }, []) diff --git a/packages/platforms/react-native/lib/client.ts b/packages/platforms/react-native/lib/client.ts index 1b786be97..ceff48b0e 100644 --- a/packages/platforms/react-native/lib/client.ts +++ b/packages/platforms/react-native/lib/client.ts @@ -43,8 +43,8 @@ const BugsnagPerformance = createClient({ deliveryFactory, idGenerator, persistence, - plugins: (spanFactory, spanContextStorage) => [ - new AppStartPlugin(appStartTime, spanFactory, clock, AppRegistry), + plugins: (spanFactory, spanContextStorage, setAppState) => [ + new AppStartPlugin(appStartTime, spanFactory, clock, AppRegistry, setAppState), new NetworkRequestPlugin(spanFactory, spanContextStorage, xhrRequestTracker) ], resourceAttributesSource, diff --git a/packages/platforms/react-native/tests/auto-instrumentation/app-start-plugin.test.ts b/packages/platforms/react-native/tests/auto-instrumentation/app-start-plugin.test.ts index ffce03791..a16ecf023 100644 --- a/packages/platforms/react-native/tests/auto-instrumentation/app-start-plugin.test.ts +++ b/packages/platforms/react-native/tests/auto-instrumentation/app-start-plugin.test.ts @@ -4,11 +4,13 @@ import createClock from '../../lib/clock' import { AppStartPlugin } from '../../lib/auto-instrumentation/app-start-plugin' import type { ReactNativeConfiguration } from '../../lib/config' import type { AppRegistry } from 'react-native' +import type { AppState } from '../../../../core/lib/core' describe('app start plugin', () => { let spanFactory: MockSpanFactory let clock: Clock let appRegistry: typeof AppRegistry + let setAppState: (appState: AppState) => void beforeEach(() => { spanFactory = new MockSpanFactory() @@ -20,7 +22,7 @@ describe('app start plugin', () => { it('starts an app start span when autoInstrumentAppStarts is true', () => { const appStartTime = 1234 - const plugin = new AppStartPlugin(appStartTime, spanFactory, clock, appRegistry) + const plugin = new AppStartPlugin(appStartTime, spanFactory, clock, appRegistry, setAppState) plugin.configure(createConfiguration({ autoInstrumentAppStarts: true })) @@ -34,7 +36,7 @@ describe('app start plugin', () => { }) it('does not start an app start span when autoInstrumentAppStarts is false', () => { - const plugin = new AppStartPlugin(1234, spanFactory, clock, appRegistry) + const plugin = new AppStartPlugin(1234, spanFactory, clock, appRegistry, setAppState) plugin.configure(createConfiguration({ autoInstrumentAppStarts: false })) diff --git a/packages/plugin-react-native-navigation/lib/react-native-navigation-plugin.ts b/packages/plugin-react-native-navigation/lib/react-native-navigation-plugin.ts index 947195205..f4516711b 100644 --- a/packages/plugin-react-native-navigation/lib/react-native-navigation-plugin.ts +++ b/packages/plugin-react-native-navigation/lib/react-native-navigation-plugin.ts @@ -1,6 +1,7 @@ import type { Plugin, SpanFactory, SpanInternal } from '@bugsnag/core-performance' import type { ReactNativeConfiguration } from '@bugsnag/react-native-performance' import type { NavigationDelegate } from 'react-native-navigation/lib/dist/src/NavigationDelegate' +import type { AppState } from '../../core/lib/core' import { createNavigationSpan } from '@bugsnag/react-native-performance' @@ -18,9 +19,11 @@ class BugsnagPluginReactNativeNavigationPerformance implements Plugin private previousRoute?: string + private setAppState: (appState: AppState) => void - constructor (Navigation: NavigationDelegate) { + constructor (Navigation: NavigationDelegate, setAppState: (appState: AppState) => void) { this.Navigation = Navigation + this.setAppState = setAppState } private clearActiveSpan () { @@ -35,6 +38,7 @@ class BugsnagPluginReactNativeNavigationPerformance implements Plugin + setAppState: (appState: AppState) => void } type EndCondition = 'condition' | 'mount' | 'unmount' | 'immediate' @@ -54,7 +56,7 @@ export class NavigationContextProvider extends React.Component { this.currentSpan.setAttribute('bugsnag.navigation.ended_by', this.endCondition) const endTime = Math.max(this.lastRenderTime, triggerNavigationEndTime) this.props.spanFactory.endSpan(this.currentSpan, endTime) - + this.props.setAppState('ready') this.currentSpan = undefined this.lastRenderTime = 0 } @@ -71,6 +73,7 @@ export class NavigationContextProvider extends React.Component { // invalid time to cause it to be discarded from the context stack. if (this.currentSpan) { spanFactory.endSpan(this.currentSpan, DISCARDED) + this.props.setAppState('ready') } const span = createNavigationSpan(spanFactory, currentRoute, { startTime: updateTime }) @@ -78,6 +81,7 @@ export class NavigationContextProvider extends React.Component { if (this.previousRoute) { span.setAttribute('bugsnag.navigation.previous_route', this.previousRoute) + this.props.setAppState('navigating') } this.currentSpan = span @@ -86,6 +90,7 @@ export class NavigationContextProvider extends React.Component { setTimeout(() => { this.triggerNavigationEnd() + this.props.setAppState('ready') }) } }