From 939b034259c2fac6a2e98624359a89e9be176fdd Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Tue, 19 Apr 2022 16:24:38 +0400 Subject: [PATCH 1/4] web: init Datadog RUM integration --- client/web/src/components/ErrorBoundary.tsx | 4 +- client/web/src/enterprise/main.tsx | 2 +- client/web/src/jscontext.ts | 6 ++ client/web/src/main.tsx | 2 +- .../src/monitoring/datadog/datadogClient.ts | 19 +++++ .../web/src/monitoring/datadog/initDatadog.ts | 73 +++++++++++++++++++ client/web/src/monitoring/index.ts | 2 + client/web/src/monitoring/initMonitoring.ts | 21 ++++++ .../web/src/monitoring/sentry/initSentry.ts | 51 +++++++++++++ .../shouldErrorBeReported.test.ts | 0 .../shouldErrorBeReported.ts | 0 client/web/src/sentry/init.ts | 68 ----------------- .../internal/app/jscontext/jscontext.go | 19 +++-- cmd/frontend/internal/app/ui/app.html | 8 ++ package.json | 1 + schema/schema.go | 22 ++++++ schema/site.schema.json | 41 +++++++++++ yarn.lock | 20 +++++ 18 files changed, 282 insertions(+), 77 deletions(-) create mode 100644 client/web/src/monitoring/datadog/datadogClient.ts create mode 100644 client/web/src/monitoring/datadog/initDatadog.ts create mode 100644 client/web/src/monitoring/index.ts create mode 100644 client/web/src/monitoring/initMonitoring.ts create mode 100644 client/web/src/monitoring/sentry/initSentry.ts rename client/web/src/{sentry => monitoring}/shouldErrorBeReported.test.ts (100%) rename client/web/src/{sentry => monitoring}/shouldErrorBeReported.ts (100%) delete mode 100644 client/web/src/sentry/init.ts diff --git a/client/web/src/components/ErrorBoundary.tsx b/client/web/src/components/ErrorBoundary.tsx index d3012b3bb216..23d08e3b7d6c 100644 --- a/client/web/src/components/ErrorBoundary.tsx +++ b/client/web/src/components/ErrorBoundary.tsx @@ -7,7 +7,7 @@ import ReloadIcon from 'mdi-react/ReloadIcon' import { asError } from '@sourcegraph/common' import { Button } from '@sourcegraph/wildcard' -import { isWebpackChunkError } from '../sentry/shouldErrorBeReported' +import { DatadogClient, isWebpackChunkError } from '../monitoring' import { HeroPage } from './HeroPage' @@ -61,6 +61,8 @@ export class ErrorBoundary extends React.PureComponent { Sentry.captureException(error) }) } + + DatadogClient.addError(error, { errorInfo, originalException: error }) } public componentDidUpdate(previousProps: Props): void { diff --git a/client/web/src/enterprise/main.tsx b/client/web/src/enterprise/main.tsx index 044d80cff41f..11ff924b0225 100644 --- a/client/web/src/enterprise/main.tsx +++ b/client/web/src/enterprise/main.tsx @@ -5,7 +5,7 @@ import '@sourcegraph/shared/src/polyfills' -import '../sentry/init' +import '../monitoring/initMonitoring' import React from 'react' diff --git a/client/web/src/jscontext.ts b/client/web/src/jscontext.ts index 5f2bce5955d7..18d366b0cfa8 100644 --- a/client/web/src/jscontext.ts +++ b/client/web/src/jscontext.ts @@ -24,6 +24,12 @@ export interface SourcegraphContext extends Pick, 'e readonly sentryDSN: string | null + /* Configuration required for Datadog RUM (https://docs.datadoghq.com/real_user_monitoring/browser/#setup). */ + readonly datadog?: { + clientToken: string + applicationId: string + } + /** Externally accessible URL for Sourcegraph (e.g., https://sourcegraph.com or http://localhost:3080). */ externalURL: string diff --git a/client/web/src/main.tsx b/client/web/src/main.tsx index 84ffc8bf1878..dccb64154f90 100644 --- a/client/web/src/main.tsx +++ b/client/web/src/main.tsx @@ -5,7 +5,7 @@ import '@sourcegraph/shared/src/polyfills' -import './sentry/init' +import './monitoring/initMonitoring' import React from 'react' diff --git a/client/web/src/monitoring/datadog/datadogClient.ts b/client/web/src/monitoring/datadog/datadogClient.ts new file mode 100644 index 000000000000..8e218c1f7450 --- /dev/null +++ b/client/web/src/monitoring/datadog/datadogClient.ts @@ -0,0 +1,19 @@ +import { ErrorInfo } from 'react' + +// Ensures consistent `ErrorContext` across `addError` calls. +// https://docs.datadoghq.com/real_user_monitoring/browser/modifying_data_and_context/?tab=npm#enrich-and-control-rum-data +interface ErrorContext { + // Used to ignore some errors in the `beforeSend` hook. + originalException: unknown + errorInfo?: ErrorInfo +} + +export const DatadogClient = { + addError: (error: unknown, context: ErrorContext): void => { + // Temporary solution for checking the availability of the + // Datadog SDK until we decide to move forward with this service. + if (typeof DD_RUM !== 'undefined') { + DD_RUM.addError(error, context) + } + }, +} diff --git a/client/web/src/monitoring/datadog/initDatadog.ts b/client/web/src/monitoring/datadog/initDatadog.ts new file mode 100644 index 000000000000..cde3d444ae70 --- /dev/null +++ b/client/web/src/monitoring/datadog/initDatadog.ts @@ -0,0 +1,73 @@ +// Import only types to avoid adding `@datadog/browser-rum-slim` to our bundle. +import type { RumGlobal } from '@datadog/browser-rum-slim' + +import { authenticatedUser } from '../../auth' +import { shouldErrorBeReported } from '../shouldErrorBeReported' + +declare global { + const DD_RUM: RumGlobal +} + +/** + * Datadog is initialized only if: + * 1. The SDK script is included into the `index.html` template (app.html). + * 2. Datadog RUM is configured using Sourcegraph site configuration. + * 3. `ENABLE_MONITORING || NODE_ENV === 'production'` to prevent log spam in the development environment. + */ +export function initDatadog(): void { + if ( + typeof DD_RUM !== 'undefined' && + window.context.datadog && + (process.env.NODE_ENV === 'production' || process.env.ENABLE_MONITORING) + ) { + const { + datadog: { applicationId, clientToken }, + version, + } = window.context + + // The SDK is loaded asynchronously via an async script defined in the `app.html`. + // https://docs.datadoghq.com/real_user_monitoring/browser/#cdn-async + DD_RUM.onReady(() => { + // Initialization parameters: https://docs.datadoghq.com/real_user_monitoring/browser/#configuration + DD_RUM.init({ + clientToken, + applicationId, + env: process.env.NODE_ENV, + // Sanitize the development version to meet Datadog tagging requirements. + // https://docs.datadoghq.com/getting_started/tagging/#defining-tags + version: version.replace('+', '_'), + // A relative sampling (in percent) to the number of sessions collected. + // https://docs.datadoghq.com/real_user_monitoring/browser/modifying_data_and_context/?tab=npm#sampling + sampleRate: 100, + // We can enable it later after verifying that basic RUM functionality works. + // https://docs.datadoghq.com/real_user_monitoring/browser/tracking_user_actions + trackInteractions: false, + // It's identical to Sentry `beforeSend` hook for now. When we decide to drop + // one of the services, we can start using more Datadog-specific properties to filter out logs. + // https://docs.datadoghq.com/real_user_monitoring/browser/modifying_data_and_context/?tab=npm#enrich-and-control-rum-data + beforeSend(event) { + const { type, context } = event + + // Use `originalException` to check if we want to ignore the error. + if (type === 'error') { + return shouldErrorBeReported(context?.originalException) + } + + return true + }, + }) + + // Datadog RUM is never un-initialized so there's no need to handle this subscription. + // eslint-disable-next-line rxjs/no-ignored-subscription + authenticatedUser.subscribe(user => { + // Add user information to a RUM session. + // https://docs.datadoghq.com/real_user_monitoring/browser/modifying_data_and_context/?tab=npm#identify-user-sessions + if (user) { + DD_RUM.setUser(user) + } else { + DD_RUM.removeUser() + } + }) + }) + } +} diff --git a/client/web/src/monitoring/index.ts b/client/web/src/monitoring/index.ts new file mode 100644 index 000000000000..b53bea837f84 --- /dev/null +++ b/client/web/src/monitoring/index.ts @@ -0,0 +1,2 @@ +export { DatadogClient } from './datadog/datadogClient' +export { isWebpackChunkError } from './shouldErrorBeReported' diff --git a/client/web/src/monitoring/initMonitoring.ts b/client/web/src/monitoring/initMonitoring.ts new file mode 100644 index 000000000000..24e23095bd6c --- /dev/null +++ b/client/web/src/monitoring/initMonitoring.ts @@ -0,0 +1,21 @@ +import { initDatadog } from './datadog/initDatadog' +import { initSentry } from './sentry/initSentry' + +window.addEventListener('error', error => { + /** + * The "ResizeObserver loop limit exceeded" error means that `ResizeObserver` was not + * able to deliver all observations within a single animation frame. It doesn't break + * the functionality of the application. The W3C considers converting this error to a warning: + * https://github.com/w3c/csswg-drafts/issues/5023 + * We can safely ignore it in the production environment to avoid hammering Sentry and other + * libraries relying on `window.addEventListener('error', callback)`. + */ + const isResizeObserverLoopError = error.message === 'ResizeObserver loop limit exceeded' + + if (process.env.NODE_ENV === 'production' && isResizeObserverLoopError) { + error.stopImmediatePropagation() + } +}) + +initSentry() +initDatadog() diff --git a/client/web/src/monitoring/sentry/initSentry.ts b/client/web/src/monitoring/sentry/initSentry.ts new file mode 100644 index 000000000000..a86320cee1eb --- /dev/null +++ b/client/web/src/monitoring/sentry/initSentry.ts @@ -0,0 +1,51 @@ +// Import only types to avoid adding `@sentry/browser` to our bundle. +import type { Hub, init, onLoad } from '@sentry/browser' + +import { authenticatedUser } from '../../auth' +import { shouldErrorBeReported } from '../shouldErrorBeReported' + +export type SentrySDK = Hub & { + init: typeof init + onLoad: typeof onLoad +} + +declare global { + const Sentry: SentrySDK +} + +export function initSentry(): void { + if ( + typeof Sentry !== 'undefined' && + window.context.sentryDSN && + (process.env.NODE_ENV === 'production' || process.env.ENABLE_MONITORING) + ) { + const { sentryDSN, version } = window.context + + // Wait for Sentry to lazy-load from the script tag defined in the `app.html`. + // https://sentry-docs-git-patch-1.sentry.dev/platforms/javascript/guides/react/install/lazy-load-sentry/ + Sentry.onLoad(() => { + Sentry.init({ + dsn: sentryDSN, + release: 'frontend@' + version, + beforeSend(event, hint) { + // Use `originalException` to check if we want to ignore the error. + if (!hint || shouldErrorBeReported(hint.originalException)) { + return event + } + + return null + }, + }) + + // Sentry is never un-initialized. + // eslint-disable-next-line rxjs/no-ignored-subscription + authenticatedUser.subscribe(user => { + Sentry.configureScope(scope => { + if (user) { + scope.setUser({ id: user.id }) + } + }) + }) + }) + } +} diff --git a/client/web/src/sentry/shouldErrorBeReported.test.ts b/client/web/src/monitoring/shouldErrorBeReported.test.ts similarity index 100% rename from client/web/src/sentry/shouldErrorBeReported.test.ts rename to client/web/src/monitoring/shouldErrorBeReported.test.ts diff --git a/client/web/src/sentry/shouldErrorBeReported.ts b/client/web/src/monitoring/shouldErrorBeReported.ts similarity index 100% rename from client/web/src/sentry/shouldErrorBeReported.ts rename to client/web/src/monitoring/shouldErrorBeReported.ts diff --git a/client/web/src/sentry/init.ts b/client/web/src/sentry/init.ts deleted file mode 100644 index 331de9feeef0..000000000000 --- a/client/web/src/sentry/init.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Import only types to avoid including @sentry/browser into the main chunk. -import type { Hub, init, onLoad } from '@sentry/browser' - -import { authenticatedUser } from '../auth' - -import { shouldErrorBeReported } from './shouldErrorBeReported' - -window.addEventListener('error', error => { - /** - * The "ResizeObserver loop limit exceeded" error means that `ResizeObserver` was not - * able to deliver all observations within a single animation frame. It doesn't break - * the functionality of the application. The W3C considers converting this error to a warning: - * https://github.com/w3c/csswg-drafts/issues/5023 - * We can safely ignore it in the production environment to avoid hammering Sentry and other - * libraries relying on `window.addEventListener('error', callback)`. - */ - const isResizeObserverLoopError = error.message === 'ResizeObserver loop limit exceeded' - - if (process.env.NODE_ENV === 'production' && isResizeObserverLoopError) { - error.stopImmediatePropagation() - } -}) - -export type SentrySDK = Hub & { - init: typeof init - onLoad: typeof onLoad -} - -declare global { - const Sentry: SentrySDK -} - -if (typeof Sentry !== 'undefined') { - // Wait for Sentry to lazy-load from the script tag defined in the app.html. - // https://sentry-docs-git-patch-1.sentry.dev/platforms/javascript/guides/react/install/lazy-load-sentry/ - Sentry.onLoad(() => { - // This check is required to please the Typescript compiler 🙂. - if (window.context.sentryDSN) { - Sentry.init({ - dsn: window.context.sentryDSN, - release: 'frontend@' + window.context.version, - beforeSend(event, hint) { - // Report errors only in production environment. - if (process.env.NODE_ENV !== 'production') { - return null - } - - // Use `originalException` to check if we want to ignore the error. - if (!hint || shouldErrorBeReported(hint.originalException)) { - return event - } - - return null - }, - }) - - // Sentry is never un-initialized. - // eslint-disable-next-line rxjs/no-ignored-subscription - authenticatedUser.subscribe(user => { - Sentry.configureScope(scope => { - if (user) { - scope.setUser({ id: user.id }) - } - }) - }) - } - }) -} diff --git a/cmd/frontend/internal/app/jscontext/jscontext.go b/cmd/frontend/internal/app/jscontext/jscontext.go index 32789c8b0c86..c36186bb677e 100644 --- a/cmd/frontend/internal/app/jscontext/jscontext.go +++ b/cmd/frontend/internal/app/jscontext/jscontext.go @@ -56,12 +56,13 @@ type JSContext struct { IsAuthenticatedUser bool `json:"isAuthenticatedUser"` - SentryDSN *string `json:"sentryDSN"` - SiteID string `json:"siteID"` - SiteGQLID string `json:"siteGQLID"` - Debug bool `json:"debug"` - NeedsSiteInit bool `json:"needsSiteInit"` - EmailEnabled bool `json:"emailEnabled"` + Datadog schema.RUM `json:"datadog,omitempty"` + SentryDSN *string `json:"sentryDSN"` + SiteID string `json:"siteID"` + SiteGQLID string `json:"siteGQLID"` + Debug bool `json:"debug"` + NeedsSiteInit bool `json:"needsSiteInit"` + EmailEnabled bool `json:"emailEnabled"` Site schema.SiteConfiguration `json:"site"` // public subset of site configuration LikelyDockerOnMac bool `json:"likelyDockerOnMac"` @@ -146,6 +147,11 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext { sentryDSN = &siteConfig.Log.Sentry.Dsn } + var datadogRUM schema.RUM + if siteConfig.ObservabilityLogging != nil && siteConfig.ObservabilityLogging.Datadog != nil && siteConfig.ObservabilityLogging.Datadog.RUM != nil { + datadogRUM = *siteConfig.ObservabilityLogging.Datadog.RUM + } + var githubAppCloudSlug string var githubAppCloudClientID string if envvar.SourcegraphDotComMode() && siteConfig.Dotcom != nil && siteConfig.Dotcom.GithubAppCloud != nil { @@ -165,6 +171,7 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext { AssetsRoot: assetsutil.URL("").String(), Version: version.Version(), IsAuthenticatedUser: actor.IsAuthenticated(), + Datadog: datadogRUM, SentryDSN: sentryDSN, Debug: env.InsecureDev, SiteID: siteID, diff --git a/cmd/frontend/internal/app/ui/app.html b/cmd/frontend/internal/app/ui/app.html index d17d96fd3603..d0785b5fa538 100644 --- a/cmd/frontend/internal/app/ui/app.html +++ b/cmd/frontend/internal/app/ui/app.html @@ -40,7 +40,15 @@ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-TB4NLS7'); + + + + + {{ end }}