Skip to content

Commit

Permalink
🪟🔧 AppMonitoringService for custom datadog RUM events (#19287)
Browse files Browse the repository at this point in the history
  • Loading branch information
josephkmh authored Nov 11, 2022
1 parent 745affb commit f3a38d8
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 43 deletions.
37 changes: 20 additions & 17 deletions airbyte-webapp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -38,23 +39,25 @@ const configProviders: ValueProvider<Config> = [envConfigProvider, windowConfigP

const Services: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => (
<AnalyticsProvider>
<ApiErrorBoundary>
<WorkspaceServiceProvider>
<FeatureService features={defaultFeatures}>
<NotificationService>
<ConfirmationModalService>
<ModalServiceProvider>
<FormChangeTrackerService>
<HelmetProvider>
<ApiServices>{children}</ApiServices>
</HelmetProvider>
</FormChangeTrackerService>
</ModalServiceProvider>
</ConfirmationModalService>
</NotificationService>
</FeatureService>
</WorkspaceServiceProvider>
</ApiErrorBoundary>
<AppMonitoringServiceProvider>
<ApiErrorBoundary>
<WorkspaceServiceProvider>
<FeatureService features={defaultFeatures}>
<NotificationService>
<ConfirmationModalService>
<ModalServiceProvider>
<FormChangeTrackerService>
<HelmetProvider>
<ApiServices>{children}</ApiServices>
</HelmetProvider>
</FormChangeTrackerService>
</ModalServiceProvider>
</ConfirmationModalService>
</NotificationService>
</FeatureService>
</WorkspaceServiceProvider>
</ApiErrorBoundary>
</AppMonitoringServiceProvider>
</AnalyticsProvider>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { datadogRum } from "@datadog/browser-rum";
import React, { createContext, useContext } from "react";

import { AppActionCodes } from "./actionCodes";

const appMonitoringContext = createContext<AppMonitoringServiceProviderValue | null>(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<string, unknown>) => void;
/**
* Log a custom error in datadog. Useful for tracking edge case errors while handling them in the UI.
*/
trackError: (error: Error, context?: Record<string, unknown>) => 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<React.PropsWithChildren<unknown>> = ({ children }) => {
const trackAction = (action: string, context?: Record<string, unknown>) => {
if (!datadogRum.getInternalContext()) {
console.debug(`trackAction(${action}) failed because RUM is not initialized.`);
return;
}

datadogRum.addAction(action, context);
};

const trackError = (error: Error, context?: Record<string, unknown>) => {
if (!datadogRum.getInternalContext()) {
console.debug(`trackError() failed because RUM is not initialized. \n`, error);
return;
}

datadogRum.addError(error, context);
};

return <appMonitoringContext.Provider value={{ trackAction, trackError }}>{children}</appMonitoringContext.Provider>;
};
Original file line number Diff line number Diff line change
@@ -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",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AppMonitoringServiceProvider, useAppMonitoringService } from "./AppMonitoringService";
export { AppActionCodes } from "./actionCodes";
53 changes: 28 additions & 25 deletions airbyte-webapp/src/packages/cloud/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -32,31 +33,33 @@ const StyleProvider: React.FC<React.PropsWithChildren<unknown>> = ({ children })

const Services: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => (
<AnalyticsProvider>
<ApiErrorBoundary>
<NotificationServiceProvider>
<ConfirmationModalService>
<ModalServiceProvider>
<FormChangeTrackerService>
<FeatureService
features={[
FeatureItem.AllowOAuthConnector,
FeatureItem.AllowSync,
FeatureItem.AllowChangeDataGeographies,
]}
>
<AppServicesProvider>
<AuthenticationProvider>
<HelmetProvider>
<IntercomProvider>{children}</IntercomProvider>
</HelmetProvider>
</AuthenticationProvider>
</AppServicesProvider>
</FeatureService>
</FormChangeTrackerService>
</ModalServiceProvider>
</ConfirmationModalService>
</NotificationServiceProvider>
</ApiErrorBoundary>
<AppMonitoringServiceProvider>
<ApiErrorBoundary>
<NotificationServiceProvider>
<ConfirmationModalService>
<ModalServiceProvider>
<FormChangeTrackerService>
<FeatureService
features={[
FeatureItem.AllowOAuthConnector,
FeatureItem.AllowSync,
FeatureItem.AllowChangeDataGeographies,
]}
>
<AppServicesProvider>
<AuthenticationProvider>
<HelmetProvider>
<IntercomProvider>{children}</IntercomProvider>
</HelmetProvider>
</AuthenticationProvider>
</AppServicesProvider>
</FeatureService>
</FormChangeTrackerService>
</ModalServiceProvider>
</ConfirmationModalService>
</NotificationServiceProvider>
</ApiErrorBoundary>
</AppMonitoringServiceProvider>
</AnalyticsProvider>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -49,6 +50,7 @@ const LDInitializationWrapper: React.FC<React.PropsWithChildren<{ apiKey: string
const analyticsService = useAnalyticsService();
const { locale } = useIntl();
const { setMessageOverwrite } = useI18nContext();
const { trackAction } = useAppMonitoringService();

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

0 comments on commit f3a38d8

Please sign in to comment.