web: initial Datadog RUM integration (#34063)

This commit is contained in:
Valery Bugakov 2022-04-21 23:09:34 -07:00 committed by GitHub
parent 251459a29e
commit b553e2dd9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 290 additions and 77 deletions

View File

@ -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<Props, State> {
Sentry.captureException(error)
})
}
DatadogClient.addError(error, { errorInfo, originalException: error })
}
public componentDidUpdate(previousProps: Props): void {

View File

@ -5,7 +5,7 @@
import '@sourcegraph/shared/src/polyfills'
import '../sentry/init'
import '../monitoring/initMonitoring'
import React from 'react'

View File

@ -24,6 +24,12 @@ export interface SourcegraphContext extends Pick<Required<SiteConfiguration>, '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

View File

@ -5,7 +5,7 @@
import '@sourcegraph/shared/src/polyfills'
import './sentry/init'
import './monitoring/initMonitoring'
import React from 'react'

View File

@ -0,0 +1,25 @@
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 function isDatadogRumAvailable(): boolean {
return typeof DD_RUM !== 'undefined'
}
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 (isDatadogRumAvailable()) {
DD_RUM.addError(error, context)
}
},
}

View File

@ -0,0 +1,75 @@
// 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'
import { isDatadogRumAvailable } from './datadogClient'
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 (
isDatadogRumAvailable() &&
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()
}
})
})
}
}

View File

@ -0,0 +1,2 @@
export { DatadogClient } from './datadog/datadogClient'
export { isWebpackChunkError } from './shouldErrorBeReported'

View File

@ -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()

View File

@ -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 })
}
})
})
})
}
}

View File

@ -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 })
}
})
})
}
})
}

View File

@ -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,

View File

@ -40,7 +40,15 @@
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-TB4NLS7');</script>
<!-- End Google Tag Manager -->
<!-- Sentry -->
<script src='https://js.sentry-cdn.com/ae2f74442b154faf90b5ff0f7cd1c618.min.js' crossorigin="anonymous"></script>
<!-- End Sentry -->
<!-- Datadog RUM -->
<script ignore-csp>(function(h,o,u,n,d) { h=h[d]=h[d]||{q:[],onReady:function(c){h.q.push(c)}}
d=o.createElement(u);d.async=1;d.src=n;n=o.getElementsByTagName(u)[0];
n.parentNode.insertBefore(d,n)})(window,document,'script',
'https://www.datadoghq-browser-agent.com/datadog-rum-v4.js','DD_RUM');</script>
<!-- End Datadog RUM -->
{{ end }}
<script ignore-csp>
window.context = {{.Context }}

View File

@ -110,6 +110,7 @@
"@babel/preset-env": "^7.14.2",
"@babel/preset-typescript": "^7.13.0",
"@babel/runtime": "^7.14.0",
"@datadog/browser-rum-slim": "^4.7.1",
"@gql2ts/from-schema": "^1.10.1",
"@gql2ts/language-typescript": "^1.9.0",
"@gql2ts/types": "^1.9.0",

View File

@ -444,6 +444,12 @@ type CustomGitFetchMapping struct {
Fetch string `json:"fetch"`
}
// Datadog description: Datadog configuration
type Datadog struct {
// RUM description: Datadog RUM configuration
RUM *RUM `json:"RUM,omitempty"`
}
// DebugLog description: Turns on debug logging for specific debugging scenarios.
type DebugLog struct {
// ExtsvcGitlab description: Log GitLab API requests.
@ -1228,6 +1234,12 @@ type ObservabilityAlerts struct {
Owners []string `json:"owners,omitempty"`
}
// ObservabilityLogging description: Configuration for logging to external services.
type ObservabilityLogging struct {
// Datadog description: Datadog configuration
Datadog *Datadog `json:"datadog,omitempty"`
}
// ObservabilityTracing description: Controls the settings for distributed tracing.
type ObservabilityTracing struct {
// Debug description: Turns on debug logging of opentracing client requests. This can be useful for debugging connectivity issues between the tracing client and the Jaeger agent, the performance overhead of tracing, and other issues related to the use of distributed tracing.
@ -1436,6 +1448,14 @@ type QuickLink struct {
Url string `json:"url"`
}
// RUM description: Datadog RUM configuration
type RUM struct {
// ApplicationId description: Datadog application ID required RUM (https://docs.datadoghq.com/real_user_monitoring/browser/#setup).
ApplicationId string `json:"applicationId"`
// ClientToken description: Datadog client token required RUM (https://docs.datadoghq.com/real_user_monitoring/browser/#setup).
ClientToken string `json:"clientToken"`
}
// Ranking description: Experimental search result ranking options.
type Ranking struct {
// MaxReorderQueueSize description: The maximum number of search results that can be buffered to sort results. -1 is unbounded. The default is 24. Set this to small integers to limit latency increases from slow backends.
@ -1863,6 +1883,8 @@ type SiteConfiguration struct {
ObservabilityLogSlowGraphQLRequests int `json:"observability.logSlowGraphQLRequests,omitempty"`
// ObservabilityLogSlowSearches description: (debug) logs all search queries (issued by users, code intelligence, or API requests) slower than the specified number of milliseconds.
ObservabilityLogSlowSearches int `json:"observability.logSlowSearches,omitempty"`
// ObservabilityLogging description: Configuration for logging to external services.
ObservabilityLogging *ObservabilityLogging `json:"observability.logging,omitempty"`
// ObservabilitySilenceAlerts description: Silence individual Sourcegraph alerts by identifier.
ObservabilitySilenceAlerts []string `json:"observability.silenceAlerts,omitempty"`
// ObservabilityTracing description: Controls the settings for distributed tracing.

View File

@ -1063,6 +1063,47 @@
"type": "string",
"examples": ["https://sourcegraph.example.com"]
},
"observability.logging": {
"description": "Configuration for logging to external services.",
"type": "object",
"additionalProperties": false,
"properties": {
"datadog": {
"description": "Datadog configuration",
"type": "object",
"additionalProperties": false,
"properties": {
"RUM": {
"description": "Datadog RUM configuration",
"type": "object",
"required": ["clientToken", "applicationId"],
"additionalProperties": false,
"properties": {
"clientToken": {
"description": "Datadog client token required RUM (https://docs.datadoghq.com/real_user_monitoring/browser/#setup).",
"type": "string"
},
"applicationId": {
"description": "Datadog application ID required RUM (https://docs.datadoghq.com/real_user_monitoring/browser/#setup).",
"type": "string"
}
}
}
}
}
},
"examples": [
{
"datadog": {
"RUM": {
"clientToken": "aaa1111a1a11a1aa11a1aa1a111aaa111aa",
"applicationId": "a11aa111-a1aa-1a11-aaa1-a11a111a111a"
}
}
}
],
"group": "Debug"
},
"observability.tracing": {
"description": "Controls the settings for distributed tracing.",
"type": "object",

View File

@ -1722,6 +1722,26 @@
dependencies:
"@cspotcode/source-map-consumer" "0.8.0"
"@datadog/browser-core@4.7.1":
version "4.7.1"
resolved "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-4.7.1.tgz#5c78028217df455ad2fc981b51035144c25ac7ab"
integrity sha512-PmHrTFYGvegheaub4sdbHmcFxwoCy2Ot/PBblSIJjfRddpbElBiSvBAOfFi3Wx8AK50a2yq0/YX9Nd/oL3Q0Dw==
"@datadog/browser-rum-core@4.7.1":
version "4.7.1"
resolved "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-4.7.1.tgz#252dfc7c72a8aa5157d27c0427c22bd28d7bc281"
integrity sha512-USlAHIvMpNczCbtpwLkn6D41GnewuP6X2D+Rn34F3PzQ16CeCXEvXrC+8cJvjy/pcsYUeDK06ATRfK0gInd0tA==
dependencies:
"@datadog/browser-core" "4.7.1"
"@datadog/browser-rum-slim@^4.7.1":
version "4.7.1"
resolved "https://registry.npmjs.org/@datadog/browser-rum-slim/-/browser-rum-slim-4.7.1.tgz#d8ff6e103085615d69a0124d585b0723dc645654"
integrity sha512-EmRIEu0LW698hnX707lwlORRgH1nsI3wtyuo/zqBDLR6rUrntIb4rNE9SL5fC6pZTg9eCb9V21sndDgIRxXn9w==
dependencies:
"@datadog/browser-core" "4.7.1"
"@datadog/browser-rum-core" "4.7.1"
"@devicefarmer/adbkit-logcat@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@devicefarmer/adbkit-logcat/-/adbkit-logcat-1.1.0.tgz#866d3406dc9f3791446adfe3ae622ffc48607db4"