diff --git a/airbyte-webapp/src/App.tsx b/airbyte-webapp/src/App.tsx index 4a18cf5bbaa8..7843bb1d6c20 100644 --- a/airbyte-webapp/src/App.tsx +++ b/airbyte-webapp/src/App.tsx @@ -8,6 +8,7 @@ import { ApiErrorBoundary } from "components/common/ApiErrorBoundary"; import { ApiServices } from "core/ApiServices"; import { I18nProvider } from "core/i18n"; import { ServicesProvider } from "core/servicesProvider"; +import { AppMonitoringServiceProvider } from "hooks/services/AppMonitoringService"; import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; import { defaultFeatures, FeatureService } from "hooks/services/Feature"; import { FormChangeTrackerService } from "hooks/services/FormChangeTracker"; @@ -38,23 +39,25 @@ const configProviders: ValueProvider = [envConfigProvider, windowConfigP const Services: React.FC> = ({ children }) => ( - - - - - - - - - {children} - - - - - - - - + + + + + + + + + + {children} + + + + + + + + + ); diff --git a/airbyte-webapp/src/hooks/services/AppMonitoringService/AppMonitoringService.tsx b/airbyte-webapp/src/hooks/services/AppMonitoringService/AppMonitoringService.tsx new file mode 100644 index 000000000000..8f44132b9700 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/AppMonitoringService/AppMonitoringService.tsx @@ -0,0 +1,56 @@ +import { datadogRum } from "@datadog/browser-rum"; +import React, { createContext, useContext } from "react"; + +import { AppActionCodes } from "./actionCodes"; + +const appMonitoringContext = createContext(null); + +/** + * The AppMonitoringService exposes methods for tracking actions and errors from the webapp. + * These methods are particularly useful for tracking when unexpected or edge-case conditions + * are encountered in production. + */ +interface AppMonitoringServiceProviderValue { + /** + * Log a custom action in datadog. Useful for tracking edge cases or unexpected application states. + */ + trackAction: (actionCode: AppActionCodes, context?: Record) => void; + /** + * Log a custom error in datadog. Useful for tracking edge case errors while handling them in the UI. + */ + trackError: (error: Error, context?: Record) => void; +} + +export const useAppMonitoringService = (): AppMonitoringServiceProviderValue => { + const context = useContext(appMonitoringContext); + if (context === null) { + throw new Error("useAppMonitoringService must be used within a AppMonitoringServiceProvider"); + } + + return context; +}; + +/** + * This implementation of the AppMonitoringService uses the datadog SDK to track errors and actions + */ +export const AppMonitoringServiceProvider: React.FC> = ({ children }) => { + const trackAction = (action: string, context?: Record) => { + if (!datadogRum.getInternalContext()) { + console.debug(`trackAction(${action}) failed because RUM is not initialized.`); + return; + } + + datadogRum.addAction(action, context); + }; + + const trackError = (error: Error, context?: Record) => { + if (!datadogRum.getInternalContext()) { + console.debug(`trackError() failed because RUM is not initialized. \n`, error); + return; + } + + datadogRum.addError(error, context); + }; + + return {children}; +}; diff --git a/airbyte-webapp/src/hooks/services/AppMonitoringService/actionCodes.ts b/airbyte-webapp/src/hooks/services/AppMonitoringService/actionCodes.ts new file mode 100644 index 000000000000..ddcbe000a02a --- /dev/null +++ b/airbyte-webapp/src/hooks/services/AppMonitoringService/actionCodes.ts @@ -0,0 +1,11 @@ +/** + * Action codes are used to log specific runtime events that we want to analyse in datadog. + * This is useful for tracking when and how frequently certain code paths on the frontend are + * encountered in production. + */ +export enum AppActionCodes { + /** + * LaunchDarkly did not load in time and was ignored + */ + LD_LOAD_TIMEOUT = "LD_LOAD_TIMEOUT", +} diff --git a/airbyte-webapp/src/hooks/services/AppMonitoringService/index.ts b/airbyte-webapp/src/hooks/services/AppMonitoringService/index.ts new file mode 100644 index 000000000000..ba69c7120194 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/AppMonitoringService/index.ts @@ -0,0 +1,2 @@ +export { AppMonitoringServiceProvider, useAppMonitoringService } from "./AppMonitoringService"; +export { AppActionCodes } from "./actionCodes"; diff --git a/airbyte-webapp/src/packages/cloud/App.tsx b/airbyte-webapp/src/packages/cloud/App.tsx index d149224407e6..18f6d7e5116c 100644 --- a/airbyte-webapp/src/packages/cloud/App.tsx +++ b/airbyte-webapp/src/packages/cloud/App.tsx @@ -7,6 +7,7 @@ import { ApiErrorBoundary } from "components/common/ApiErrorBoundary"; import LoadingPage from "components/LoadingPage"; import { I18nProvider } from "core/i18n"; +import { AppMonitoringServiceProvider } from "hooks/services/AppMonitoringService"; import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; import { FeatureItem, FeatureService } from "hooks/services/Feature"; import { FormChangeTrackerService } from "hooks/services/FormChangeTracker"; @@ -32,31 +33,33 @@ const StyleProvider: React.FC> = ({ children }) const Services: React.FC> = ({ children }) => ( - - - - - - - - - - {children} - - - - - - - - - + + + + + + + + + + + {children} + + + + + + + + + + ); diff --git a/airbyte-webapp/src/packages/cloud/services/thirdParty/launchdarkly/LDExperimentService.tsx b/airbyte-webapp/src/packages/cloud/services/thirdParty/launchdarkly/LDExperimentService.tsx index 9729ecd8dc4f..0c8881f9e3cc 100644 --- a/airbyte-webapp/src/packages/cloud/services/thirdParty/launchdarkly/LDExperimentService.tsx +++ b/airbyte-webapp/src/packages/cloud/services/thirdParty/launchdarkly/LDExperimentService.tsx @@ -9,6 +9,7 @@ import { LoadingPage } from "components"; import { useConfig } from "config"; import { useI18nContext } from "core/i18n"; import { useAnalyticsService } from "hooks/services/Analytics"; +import { useAppMonitoringService, AppActionCodes } from "hooks/services/AppMonitoringService"; import { ExperimentProvider, ExperimentService } from "hooks/services/Experiment"; import type { Experiments } from "hooks/services/Experiment/experiments"; import { FeatureSet, useFeatureService } from "hooks/services/Feature"; @@ -49,6 +50,7 @@ const LDInitializationWrapper: React.FC { // The LaunchDarkly promise resolved before the timeout, so we're good to use LD. @@ -103,6 +105,9 @@ const LDInitializationWrapper: React.FC