Skip to content

Commit f3a38d8

Browse files
author
Joey Marshment-Howell
authored
🪟🔧 AppMonitoringService for custom datadog RUM events (#19287)
1 parent 745affb commit f3a38d8

File tree

6 files changed

+123
-43
lines changed

6 files changed

+123
-43
lines changed

airbyte-webapp/src/App.tsx

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ApiErrorBoundary } from "components/common/ApiErrorBoundary";
88
import { ApiServices } from "core/ApiServices";
99
import { I18nProvider } from "core/i18n";
1010
import { ServicesProvider } from "core/servicesProvider";
11+
import { AppMonitoringServiceProvider } from "hooks/services/AppMonitoringService";
1112
import { ConfirmationModalService } from "hooks/services/ConfirmationModal";
1213
import { defaultFeatures, FeatureService } from "hooks/services/Feature";
1314
import { FormChangeTrackerService } from "hooks/services/FormChangeTracker";
@@ -38,23 +39,25 @@ const configProviders: ValueProvider<Config> = [envConfigProvider, windowConfigP
3839

3940
const Services: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => (
4041
<AnalyticsProvider>
41-
<ApiErrorBoundary>
42-
<WorkspaceServiceProvider>
43-
<FeatureService features={defaultFeatures}>
44-
<NotificationService>
45-
<ConfirmationModalService>
46-
<ModalServiceProvider>
47-
<FormChangeTrackerService>
48-
<HelmetProvider>
49-
<ApiServices>{children}</ApiServices>
50-
</HelmetProvider>
51-
</FormChangeTrackerService>
52-
</ModalServiceProvider>
53-
</ConfirmationModalService>
54-
</NotificationService>
55-
</FeatureService>
56-
</WorkspaceServiceProvider>
57-
</ApiErrorBoundary>
42+
<AppMonitoringServiceProvider>
43+
<ApiErrorBoundary>
44+
<WorkspaceServiceProvider>
45+
<FeatureService features={defaultFeatures}>
46+
<NotificationService>
47+
<ConfirmationModalService>
48+
<ModalServiceProvider>
49+
<FormChangeTrackerService>
50+
<HelmetProvider>
51+
<ApiServices>{children}</ApiServices>
52+
</HelmetProvider>
53+
</FormChangeTrackerService>
54+
</ModalServiceProvider>
55+
</ConfirmationModalService>
56+
</NotificationService>
57+
</FeatureService>
58+
</WorkspaceServiceProvider>
59+
</ApiErrorBoundary>
60+
</AppMonitoringServiceProvider>
5861
</AnalyticsProvider>
5962
);
6063

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { datadogRum } from "@datadog/browser-rum";
2+
import React, { createContext, useContext } from "react";
3+
4+
import { AppActionCodes } from "./actionCodes";
5+
6+
const appMonitoringContext = createContext<AppMonitoringServiceProviderValue | null>(null);
7+
8+
/**
9+
* The AppMonitoringService exposes methods for tracking actions and errors from the webapp.
10+
* These methods are particularly useful for tracking when unexpected or edge-case conditions
11+
* are encountered in production.
12+
*/
13+
interface AppMonitoringServiceProviderValue {
14+
/**
15+
* Log a custom action in datadog. Useful for tracking edge cases or unexpected application states.
16+
*/
17+
trackAction: (actionCode: AppActionCodes, context?: Record<string, unknown>) => void;
18+
/**
19+
* Log a custom error in datadog. Useful for tracking edge case errors while handling them in the UI.
20+
*/
21+
trackError: (error: Error, context?: Record<string, unknown>) => void;
22+
}
23+
24+
export const useAppMonitoringService = (): AppMonitoringServiceProviderValue => {
25+
const context = useContext(appMonitoringContext);
26+
if (context === null) {
27+
throw new Error("useAppMonitoringService must be used within a AppMonitoringServiceProvider");
28+
}
29+
30+
return context;
31+
};
32+
33+
/**
34+
* This implementation of the AppMonitoringService uses the datadog SDK to track errors and actions
35+
*/
36+
export const AppMonitoringServiceProvider: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
37+
const trackAction = (action: string, context?: Record<string, unknown>) => {
38+
if (!datadogRum.getInternalContext()) {
39+
console.debug(`trackAction(${action}) failed because RUM is not initialized.`);
40+
return;
41+
}
42+
43+
datadogRum.addAction(action, context);
44+
};
45+
46+
const trackError = (error: Error, context?: Record<string, unknown>) => {
47+
if (!datadogRum.getInternalContext()) {
48+
console.debug(`trackError() failed because RUM is not initialized. \n`, error);
49+
return;
50+
}
51+
52+
datadogRum.addError(error, context);
53+
};
54+
55+
return <appMonitoringContext.Provider value={{ trackAction, trackError }}>{children}</appMonitoringContext.Provider>;
56+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Action codes are used to log specific runtime events that we want to analyse in datadog.
3+
* This is useful for tracking when and how frequently certain code paths on the frontend are
4+
* encountered in production.
5+
*/
6+
export enum AppActionCodes {
7+
/**
8+
* LaunchDarkly did not load in time and was ignored
9+
*/
10+
LD_LOAD_TIMEOUT = "LD_LOAD_TIMEOUT",
11+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { AppMonitoringServiceProvider, useAppMonitoringService } from "./AppMonitoringService";
2+
export { AppActionCodes } from "./actionCodes";

airbyte-webapp/src/packages/cloud/App.tsx

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ApiErrorBoundary } from "components/common/ApiErrorBoundary";
77
import LoadingPage from "components/LoadingPage";
88

99
import { I18nProvider } from "core/i18n";
10+
import { AppMonitoringServiceProvider } from "hooks/services/AppMonitoringService";
1011
import { ConfirmationModalService } from "hooks/services/ConfirmationModal";
1112
import { FeatureItem, FeatureService } from "hooks/services/Feature";
1213
import { FormChangeTrackerService } from "hooks/services/FormChangeTracker";
@@ -32,31 +33,33 @@ const StyleProvider: React.FC<React.PropsWithChildren<unknown>> = ({ children })
3233

3334
const Services: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => (
3435
<AnalyticsProvider>
35-
<ApiErrorBoundary>
36-
<NotificationServiceProvider>
37-
<ConfirmationModalService>
38-
<ModalServiceProvider>
39-
<FormChangeTrackerService>
40-
<FeatureService
41-
features={[
42-
FeatureItem.AllowOAuthConnector,
43-
FeatureItem.AllowSync,
44-
FeatureItem.AllowChangeDataGeographies,
45-
]}
46-
>
47-
<AppServicesProvider>
48-
<AuthenticationProvider>
49-
<HelmetProvider>
50-
<IntercomProvider>{children}</IntercomProvider>
51-
</HelmetProvider>
52-
</AuthenticationProvider>
53-
</AppServicesProvider>
54-
</FeatureService>
55-
</FormChangeTrackerService>
56-
</ModalServiceProvider>
57-
</ConfirmationModalService>
58-
</NotificationServiceProvider>
59-
</ApiErrorBoundary>
36+
<AppMonitoringServiceProvider>
37+
<ApiErrorBoundary>
38+
<NotificationServiceProvider>
39+
<ConfirmationModalService>
40+
<ModalServiceProvider>
41+
<FormChangeTrackerService>
42+
<FeatureService
43+
features={[
44+
FeatureItem.AllowOAuthConnector,
45+
FeatureItem.AllowSync,
46+
FeatureItem.AllowChangeDataGeographies,
47+
]}
48+
>
49+
<AppServicesProvider>
50+
<AuthenticationProvider>
51+
<HelmetProvider>
52+
<IntercomProvider>{children}</IntercomProvider>
53+
</HelmetProvider>
54+
</AuthenticationProvider>
55+
</AppServicesProvider>
56+
</FeatureService>
57+
</FormChangeTrackerService>
58+
</ModalServiceProvider>
59+
</ConfirmationModalService>
60+
</NotificationServiceProvider>
61+
</ApiErrorBoundary>
62+
</AppMonitoringServiceProvider>
6063
</AnalyticsProvider>
6164
);
6265

airbyte-webapp/src/packages/cloud/services/thirdParty/launchdarkly/LDExperimentService.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { LoadingPage } from "components";
99
import { useConfig } from "config";
1010
import { useI18nContext } from "core/i18n";
1111
import { useAnalyticsService } from "hooks/services/Analytics";
12+
import { useAppMonitoringService, AppActionCodes } from "hooks/services/AppMonitoringService";
1213
import { ExperimentProvider, ExperimentService } from "hooks/services/Experiment";
1314
import type { Experiments } from "hooks/services/Experiment/experiments";
1415
import { FeatureSet, useFeatureService } from "hooks/services/Feature";
@@ -49,6 +50,7 @@ const LDInitializationWrapper: React.FC<React.PropsWithChildren<{ apiKey: string
4950
const analyticsService = useAnalyticsService();
5051
const { locale } = useIntl();
5152
const { setMessageOverwrite } = useI18nContext();
53+
const { trackAction } = useAppMonitoringService();
5254

5355
/**
5456
* This function checks for all experiments to find the ones beginning with "i18n_{locale}_"
@@ -87,7 +89,7 @@ const LDInitializationWrapper: React.FC<React.PropsWithChildren<{ apiKey: string
8789
// Wait for either LaunchDarkly to initialize or a specific timeout to pass first
8890
Promise.race([
8991
ldClient.current.waitForInitialization(),
90-
rejectAfter(INITIALIZATION_TIMEOUT, "Timed out waiting for LaunchDarkly to initialize"),
92+
rejectAfter(INITIALIZATION_TIMEOUT, AppActionCodes.LD_LOAD_TIMEOUT),
9193
])
9294
.then(() => {
9395
// The LaunchDarkly promise resolved before the timeout, so we're good to use LD.
@@ -103,6 +105,9 @@ const LDInitializationWrapper: React.FC<React.PropsWithChildren<{ apiKey: string
103105
// our timeout promise resolves first, we're going to show an error and assume the service
104106
// failed to initialize, i.e. we'll run without it.
105107
console.warn(`Failed to initialize LaunchDarkly service with reason: ${String(reason)}`);
108+
if (reason === AppActionCodes.LD_LOAD_TIMEOUT) {
109+
trackAction(AppActionCodes.LD_LOAD_TIMEOUT);
110+
}
106111
setState("failed");
107112
});
108113
}

0 commit comments

Comments
 (0)