Frontend logging: Remove Sentry javascript agent support (#67493)

* remove Sentry

* fix sourcemap resolve
pull/67646/head
Domas 2 years ago committed by GitHub
parent 9614dc2446
commit 15d4169813
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .github/renovate.json5
  2. 11
      conf/defaults.ini
  3. 15
      conf/sample.ini
  4. 20
      docs/sources/setup-grafana/configure-grafana/_index.md
  5. 1
      go.mod
  6. 4
      go.sum
  7. 3
      package.json
  8. 13
      packages/grafana-data/src/types/config.ts
  9. 1
      packages/grafana-runtime/package.json
  10. 6
      packages/grafana-runtime/src/config.ts
  11. 1
      packages/grafana-runtime/src/services/EchoSrv.ts
  12. 41
      packages/grafana-runtime/src/utils/logging.ts
  13. 1
      packages/grafana-ui/package.json
  14. 11
      packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx
  15. 2
      packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx
  16. 1
      pkg/api/dtos/frontend_settings.go
  17. 2
      pkg/api/dtos/index.go
  18. 76
      pkg/api/frontend_logging.go
  19. 290
      pkg/api/frontend_logging_test.go
  20. 2
      pkg/api/frontendlogging/grafana_javascript_agent.go
  21. 30
      pkg/api/frontendlogging/grafana_javascript_agent_sourcemaps.go
  22. 108
      pkg/api/frontendlogging/sentry.go
  23. 9
      pkg/api/frontendlogging/source_maps.go
  24. 1
      pkg/api/frontendsettings.go
  25. 1
      pkg/api/index.go
  26. 4
      pkg/setting/setting.go
  27. 21
      pkg/setting/setting_grafana_javascript_agent.go
  28. 28
      pkg/setting/setting_sentry.go
  29. 10
      public/app/app.ts
  30. 8
      public/app/core/services/echo/EchoSrv.ts
  31. 143
      public/app/core/services/echo/backends/sentry/SentryBackend.test.ts
  32. 66
      public/app/core/services/echo/backends/sentry/SentryBackend.ts
  33. 133
      public/app/core/services/echo/backends/sentry/transports/CustomEndpointTransport.test.ts
  34. 151
      public/app/core/services/echo/backends/sentry/transports/CustomEndpointTransport.ts
  35. 26
      public/app/core/services/echo/backends/sentry/transports/EchoSrvTransport.ts
  36. 16
      public/app/core/services/echo/backends/sentry/types.ts
  37. 2
      public/app/features/alerting/unified/Analytics.ts
  38. 71
      yarn.lock

@ -16,12 +16,6 @@
"systemjs", "systemjs",
"ts-loader", // we should remove ts-loader and use babel-loader instead "ts-loader", // we should remove ts-loader and use babel-loader instead
"ora", // we should bump this once we move to esm modules "ora", // we should bump this once we move to esm modules
// Sentry deprecated in favor of Grafana Faro for frontend logging.
// Major effort required to upgrade to latest Sentry, not worthwhile
"@sentry/browser",
"@sentry/types",
"@sentry/utils",
], ],
"includePaths": ["package.json", "packages/**"], "includePaths": ["package.json", "packages/**"],
"ignorePaths": ["packages/grafana-toolkit/package.json", "emails/**", "plugins-bundled/**", "**/mocks/**"], "ignorePaths": ["packages/grafana-toolkit/package.json", "emails/**", "plugins-bundled/**", "**/mocks/**"],

@ -872,21 +872,12 @@ facility =
tag = tag =
[log.frontend] [log.frontend]
# Should Sentry javascript agent be initialized # Should Faro javascript agent be initialized
enabled = false enabled = false
# Defines which provider to use sentry or grafana
provider = sentry
# Sentry DSN if you want to send events to Sentry.
sentry_dsn =
# Custom HTTP endpoint to send events to. Default will log the events to stdout. # Custom HTTP endpoint to send events to. Default will log the events to stdout.
custom_endpoint = custom_endpoint =
# Rate of events to be reported to Sentry between 0 (none) and 1 (all), float
sample_rate = 1.0
# Requests per second limit enforced per an extended period, for Grafana backend log ingestion endpoint (/log). # Requests per second limit enforced per an extended period, for Grafana backend log ingestion endpoint (/log).
log_endpoint_requests_per_second_limit = 3 log_endpoint_requests_per_second_limit = 3

@ -844,20 +844,11 @@
;tag = ;tag =
[log.frontend] [log.frontend]
# Should Sentry javascript agent be initialized # Should Faro javascript agent be initialized
;enabled = false ;enabled = false
# Defines which provider to use, default is Sentry # Custom HTTP endpoint to send events to. Default will log the events to stdout.
;provider = sentry ;custom_endpoint = /log-grafana-javascript-agent
# Sentry DSN if you want to send events to Sentry.
;sentry_dsn =
# Custom HTTP endpoint to send events captured by the Sentry agent to. Default will log the events to stdout.
;custom_endpoint = /log
# Rate of events to be reported between 0 (none) and 1 (all), float
;sample_rate = 1.0
# Requests per second limit enforced an extended period, for Grafana backend log ingestion endpoint (/log). # Requests per second limit enforced an extended period, for Grafana backend log ingestion endpoint (/log).
;log_endpoint_requests_per_second_limit = 3 ;log_endpoint_requests_per_second_limit = 3

@ -1284,31 +1284,19 @@ Syslog tag. By default, the process's `argv[0]` is used.
### enabled ### enabled
Sentry javascript agent is initialized. Default is `false`. Faro javascript agent is initialized. Default is `false`.
### provider
Defines which provider to use `sentry` or `grafana`. Default is `sentry`
### sentry_dsn
Sentry DSN if you want to send events to Sentry
### custom_endpoint ### custom_endpoint
Custom HTTP endpoint to send events captured by the Sentry agent to. Default, `/log`, will log the events to stdout. Custom HTTP endpoint to send events captured by the Faro agent to. Default, `/log-grafana-javascript-agent`, will log the events to stdout.
### sample_rate
Rate of events to be reported between `0` (none) and `1` (all, default), float.
### log_endpoint_requests_per_second_limit ### log_endpoint_requests_per_second_limit
Requests per second limit enforced per an extended period, for Grafana backend log ingestion endpoint, `/log`. Default is `3`. Requests per second limit enforced per an extended period, for Grafana backend log ingestion endpoint, `/log-grafana-javascript-agent`. Default is `3`.
### log_endpoint_burst_limit ### log_endpoint_burst_limit
Maximum requests accepted per short interval of time for Grafana backend log ingestion endpoint, `/log`. Default is `15`. Maximum requests accepted per short interval of time for Grafana backend log ingestion endpoint, `/log-grafana-javascript-agent`. Default is `15`.
### instrumentations_errors_enabled ### instrumentations_errors_enabled

@ -40,7 +40,6 @@ require (
github.com/crewjam/saml v0.4.12 github.com/crewjam/saml v0.4.12
github.com/fatih/color v1.15.0 github.com/fatih/color v1.15.0
github.com/gchaincl/sqlhooks v1.3.0 github.com/gchaincl/sqlhooks v1.3.0
github.com/getsentry/sentry-go v0.13.0
github.com/go-git/go-git/v5 v5.4.2 github.com/go-git/go-git/v5 v5.4.2
github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-ldap/ldap/v3 v3.4.4
github.com/go-openapi/strfmt v0.21.7 github.com/go-openapi/strfmt v0.21.7

@ -801,8 +801,6 @@ github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebK
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/getkin/kin-openapi v0.115.0 h1:c8WHRLVY3G8m9jQTy0/DnIuljgRwTCB5twZytQS4JyU= github.com/getkin/kin-openapi v0.115.0 h1:c8WHRLVY3G8m9jQTy0/DnIuljgRwTCB5twZytQS4JyU=
github.com/getkin/kin-openapi v0.115.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= github.com/getkin/kin-openapi v0.115.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew= github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
@ -822,7 +820,6 @@ github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
@ -1962,7 +1959,6 @@ github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=

@ -287,9 +287,6 @@
"@react-stately/tree": "3.3.1", "@react-stately/tree": "3.3.1",
"@reduxjs/toolkit": "1.9.3", "@reduxjs/toolkit": "1.9.3",
"@remix-run/router": "^1.5.0", "@remix-run/router": "^1.5.0",
"@sentry/browser": "6.19.7",
"@sentry/types": "6.19.7",
"@sentry/utils": "6.19.7",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@types/react-resizable": "3.0.3", "@types/react-resizable": "3.0.3",
"@types/trusted-types": "2.0.3", "@types/trusted-types": "2.0.3",

@ -46,18 +46,6 @@ export interface LicenseInfo {
trialExpiry?: number; trialExpiry?: number;
} }
/**
* Describes Sentry integration config
*
* @public
*/
export interface SentryConfig {
enabled: boolean;
dsn: string;
customEndpoint: string;
sampleRate: number;
}
/** /**
* Describes GrafanaJavascriptAgentConfig integration config * Describes GrafanaJavascriptAgentConfig integration config
* *
@ -209,7 +197,6 @@ export interface GrafanaConfig {
licenseInfo: LicenseInfo; licenseInfo: LicenseInfo;
http2Enabled: boolean; http2Enabled: boolean;
dateFormats?: SystemDateFormatSettings; dateFormats?: SystemDateFormatSettings;
sentry: SentryConfig;
grafanaJavascriptAgent: GrafanaJavascriptAgentConfig; grafanaJavascriptAgent: GrafanaJavascriptAgentConfig;
customTheme?: any; customTheme?: any;
geomapDefaultBaseLayer?: MapLayerOptions; geomapDefaultBaseLayer?: MapLayerOptions;

@ -41,7 +41,6 @@
"@grafana/e2e-selectors": "10.1.0-pre", "@grafana/e2e-selectors": "10.1.0-pre",
"@grafana/faro-web-sdk": "1.0.2", "@grafana/faro-web-sdk": "1.0.2",
"@grafana/ui": "10.1.0-pre", "@grafana/ui": "10.1.0-pre",
"@sentry/browser": "6.19.7",
"history": "4.10.1", "history": "4.10.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"rxjs": "7.8.0", "rxjs": "7.8.0",

@ -103,12 +103,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
supportBundlesEnabled = false; supportBundlesEnabled = false;
http2Enabled = false; http2Enabled = false;
dateFormats?: SystemDateFormatSettings; dateFormats?: SystemDateFormatSettings;
sentry = {
enabled: false,
dsn: '',
customEndpoint: '',
sampleRate: 1,
};
grafanaJavascriptAgent = { grafanaJavascriptAgent = {
enabled: false, enabled: false,
customEndpoint: '', customEndpoint: '',

@ -78,7 +78,6 @@ export interface EchoEvent<T extends EchoEventType = any, P = any> {
export enum EchoEventType { export enum EchoEventType {
Performance = 'performance', Performance = 'performance',
MetaAnalytics = 'meta-analytics', MetaAnalytics = 'meta-analytics',
Sentry = 'sentry',
Pageview = 'pageview', Pageview = 'pageview',
Interaction = 'interaction', Interaction = 'interaction',
ExperimentView = 'experimentview', ExperimentView = 'experimentview',

@ -1,76 +1,54 @@
import { captureMessage, captureException, Severity as LogLevel } from '@sentry/browser'; import { faro, LogLevel } from '@grafana/faro-web-sdk';
import { faro, LogLevel as GrafanaLogLevel } from '@grafana/faro-web-sdk';
import { config } from '../config'; import { config } from '../config';
export { LogLevel }; export { LogLevel };
// a bit stricter than what Sentry allows
type Contexts = Record<string, Record<string, number | string | Record<string, string | number>>>; type Contexts = Record<string, Record<string, number | string | Record<string, string | number>>>;
/** /**
* Log a message at INFO level. Depending on configuration might be forwarded to backend and logged to stdout or sent to Sentry * Log a message at INFO level
*
* @public * @public
*/ */
export function logInfo(message: string, contexts?: Contexts) { export function logInfo(message: string, contexts?: Contexts) {
if (config.grafanaJavascriptAgent.enabled) { if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushLog([message], { faro.api.pushLog([message], {
level: GrafanaLogLevel.INFO, level: LogLevel.INFO,
context: contexts, context: contexts,
}); });
} }
if (config.sentry.enabled) {
captureMessage(message, {
level: LogLevel.Info,
contexts,
});
}
} }
/** /**
* Log a message at WARNING level. Depending on configuration might be forwarded to backend and logged to stdout or sent to Sentry * Log a message at WARNING level
* *
* @public * @public
*/ */
export function logWarning(message: string, contexts?: Contexts) { export function logWarning(message: string, contexts?: Contexts) {
if (config.grafanaJavascriptAgent.enabled) { if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushLog([message], { faro.api.pushLog([message], {
level: GrafanaLogLevel.WARN, level: LogLevel.WARN,
context: contexts, context: contexts,
}); });
} }
if (config.sentry.enabled) {
captureMessage(message, {
level: LogLevel.Warning,
contexts,
});
}
} }
/** /**
* Log a message at DEBUG level. Depending on configuration might be forwarded to backend and logged to stdout or sent to Sentry * Log a message at DEBUG level
* *
* @public * @public
*/ */
export function logDebug(message: string, contexts?: Contexts) { export function logDebug(message: string, contexts?: Contexts) {
if (config.grafanaJavascriptAgent.enabled) { if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushLog([message], { faro.api.pushLog([message], {
level: GrafanaLogLevel.DEBUG, level: LogLevel.DEBUG,
context: contexts, context: contexts,
}); });
} }
if (config.sentry.enabled) {
captureMessage(message, {
level: LogLevel.Debug,
contexts,
});
}
} }
/** /**
* Log an error. Depending on configuration might be forwarded to backend and logged to stdout or sent to Sentry * Log an error
* *
* @public * @public
*/ */
@ -78,7 +56,4 @@ export function logError(err: Error, contexts?: Contexts) {
if (config.grafanaJavascriptAgent.enabled) { if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushError(err); faro.api.pushError(err);
} }
if (config.sentry.enabled) {
captureException(err, { contexts });
}
} }

@ -63,7 +63,6 @@
"@react-aria/overlays": "3.10.1", "@react-aria/overlays": "3.10.1",
"@react-aria/utils": "3.13.1", "@react-aria/utils": "3.13.1",
"@react-stately/menu": "3.4.1", "@react-stately/menu": "3.4.1",
"@sentry/browser": "6.19.7",
"ansicolor": "1.1.100", "ansicolor": "1.1.100",
"calculate-size": "1.1.1", "calculate-size": "1.1.1",
"classnames": "2.3.2", "classnames": "2.3.2",

@ -1,4 +1,3 @@
import { captureException } from '@sentry/browser';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React, { FC } from 'react'; import React, { FC } from 'react';
@ -6,7 +5,6 @@ import { faro } from '@grafana/faro-web-sdk';
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from './ErrorBoundary';
jest.mock('@sentry/browser');
jest.mock('@grafana/faro-web-sdk', () => ({ jest.mock('@grafana/faro-web-sdk', () => ({
faro: { faro: {
api: { api: {
@ -31,7 +29,7 @@ describe('ErrorBoundary', () => {
consoleSpy.mockRestore(); consoleSpy.mockRestore();
}); });
it('should catch error and report it to sentry, including react component stack in context', async () => { it('should catch error and report it to Faro', async () => {
const problem = new Error('things went terribly wrong'); const problem = new Error('things went terribly wrong');
render( render(
<ErrorBoundary> <ErrorBoundary>
@ -46,13 +44,6 @@ describe('ErrorBoundary', () => {
); );
await screen.findByText(problem.message); await screen.findByText(problem.message);
expect(captureException).toHaveBeenCalledTimes(1);
const [error, context] = (captureException as jest.Mock).mock.calls[0];
expect(error).toBe(problem);
expect(context).toHaveProperty('contexts');
expect(context.contexts).toHaveProperty('react');
expect(context.contexts.react).toHaveProperty('componentStack');
expect(context.contexts.react.componentStack).toMatch(/^\s+at ErrorThrower (.*)\s+at ErrorBoundary (.*)\s*$/);
expect(faro.api.pushError).toHaveBeenCalledTimes(1); expect(faro.api.pushError).toHaveBeenCalledTimes(1);
expect((faro.api.pushError as jest.Mock).mock.calls[0][0]).toBe(problem); expect((faro.api.pushError as jest.Mock).mock.calls[0][0]).toBe(problem);
}); });

@ -1,4 +1,3 @@
import { captureException } from '@sentry/browser';
import React, { PureComponent, ReactNode, ComponentType } from 'react'; import React, { PureComponent, ReactNode, ComponentType } from 'react';
import { faro } from '@grafana/faro-web-sdk'; import { faro } from '@grafana/faro-web-sdk';
@ -38,7 +37,6 @@ export class ErrorBoundary extends PureComponent<Props, State> {
}; };
componentDidCatch(error: Error, errorInfo: ErrorInfo) { componentDidCatch(error: Error, errorInfo: ErrorInfo) {
captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack } } });
faro?.api?.pushError(error); faro?.api?.pushError(error);
this.setState({ error, errorInfo }); this.setState({ error, errorInfo });

@ -185,7 +185,6 @@ type FrontendSettingsDTO struct {
RendererVersion string `json:"rendererVersion"` RendererVersion string `json:"rendererVersion"`
SecretsManagerPluginEnabled bool `json:"secretsManagerPluginEnabled"` SecretsManagerPluginEnabled bool `json:"secretsManagerPluginEnabled"`
Http2Enabled bool `json:"http2Enabled"` Http2Enabled bool `json:"http2Enabled"`
Sentry setting.Sentry `json:"sentry"`
GrafanaJavascriptAgent setting.GrafanaJavascriptAgent `json:"grafanaJavascriptAgent"` GrafanaJavascriptAgent setting.GrafanaJavascriptAgent `json:"grafanaJavascriptAgent"`
PluginCatalogURL string `json:"pluginCatalogURL"` PluginCatalogURL string `json:"pluginCatalogURL"`
PluginAdminEnabled bool `json:"pluginAdminEnabled"` PluginAdminEnabled bool `json:"pluginAdminEnabled"`

@ -4,7 +4,6 @@ import (
"html/template" "html/template"
"github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/setting"
) )
type IndexViewData struct { type IndexViewData struct {
@ -27,7 +26,6 @@ type IndexViewData struct {
FavIcon template.URL FavIcon template.URL
AppleTouchIcon template.URL AppleTouchIcon template.URL
AppTitle string AppTitle string
Sentry *setting.Sentry
ContentDeliveryURL string ContentDeliveryURL string
LoadingLogo template.URL LoadingLogo template.URL
CSPContent string CSPContent string

@ -4,7 +4,6 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/getsentry/sentry-go"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"github.com/grafana/grafana/pkg/api/frontendlogging" "github.com/grafana/grafana/pkg/api/frontendlogging"
@ -16,51 +15,8 @@ var frontendLogger = log.New("frontend")
type frontendLogMessageHandler func(hs *HTTPServer, c *web.Context) type frontendLogMessageHandler func(hs *HTTPServer, c *web.Context)
const sentryLogEndpointPath = "/log"
const grafanaJavascriptAgentEndpointPath = "/log-grafana-javascript-agent" const grafanaJavascriptAgentEndpointPath = "/log-grafana-javascript-agent"
/** @deprecated will be removed in the next major version */
func NewFrontendLogMessageHandler(store *frontendlogging.SourceMapStore) frontendLogMessageHandler {
return func(hs *HTTPServer, c *web.Context) {
event := frontendlogging.FrontendSentryEvent{}
if err := web.Bind(c.Req, &event); err != nil {
c.Resp.WriteHeader(http.StatusBadRequest)
_, err = c.Resp.Write([]byte("bad request data"))
if err != nil {
hs.log.Error("could not write to response", "err", err)
}
return
}
var msg = "unknown"
if len(event.Message) > 0 {
msg = event.Message
} else if event.Exception != nil && len(event.Exception.Values) > 0 {
msg = event.Exception.Values[0].FmtMessage()
}
var ctx = event.ToLogContext(store)
switch event.Level {
case sentry.LevelError:
frontendLogger.Error(msg, ctx...)
case sentry.LevelWarning:
frontendLogger.Warn(msg, ctx...)
case sentry.LevelDebug:
frontendLogger.Debug(msg, ctx...)
default:
frontendLogger.Info(msg, ctx...)
}
c.Resp.WriteHeader(http.StatusAccepted)
_, err := c.Resp.Write([]byte("OK"))
if err != nil {
hs.log.Error("could not write to response", "err", err)
}
}
}
func GrafanaJavascriptAgentLogMessageHandler(store *frontendlogging.SourceMapStore) frontendLogMessageHandler { func GrafanaJavascriptAgentLogMessageHandler(store *frontendlogging.SourceMapStore) frontendLogMessageHandler {
return func(hs *HTTPServer, c *web.Context) { return func(hs *HTTPServer, c *web.Context) {
event := frontendlogging.FrontendGrafanaJavascriptAgentEvent{} event := frontendlogging.FrontendGrafanaJavascriptAgentEvent{}
@ -143,9 +99,9 @@ func GrafanaJavascriptAgentLogMessageHandler(store *frontendlogging.SourceMapSto
// this is to avoid reporting errors in case config was changes but there are browser // this is to avoid reporting errors in case config was changes but there are browser
// sessions still open with older config // sessions still open with older config
func (hs *HTTPServer) frontendLogEndpoints() web.Handler { func (hs *HTTPServer) frontendLogEndpoints() web.Handler {
if !(hs.Cfg.GrafanaJavascriptAgent.Enabled || hs.Cfg.Sentry.Enabled) { if !(hs.Cfg.GrafanaJavascriptAgent.Enabled) {
return func(ctx *web.Context) { return func(ctx *web.Context) {
if ctx.Req.Method == http.MethodPost && (ctx.Req.URL.Path == sentryLogEndpointPath || ctx.Req.URL.Path == grafanaJavascriptAgentEndpointPath) { if ctx.Req.Method == http.MethodPost && ctx.Req.URL.Path == grafanaJavascriptAgentEndpointPath {
ctx.Resp.WriteHeader(http.StatusAccepted) ctx.Resp.WriteHeader(http.StatusAccepted)
_, err := ctx.Resp.Write([]byte("OK")) _, err := ctx.Resp.Write([]byte("OK"))
if err != nil { if err != nil {
@ -156,33 +112,11 @@ func (hs *HTTPServer) frontendLogEndpoints() web.Handler {
} }
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS) sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS)
rateLimiter := rate.NewLimiter(rate.Limit(hs.Cfg.GrafanaJavascriptAgent.EndpointRPS), hs.Cfg.GrafanaJavascriptAgent.EndpointBurst)
var rateLimiter *rate.Limiter handler := GrafanaJavascriptAgentLogMessageHandler(sourceMapStore)
var handler frontendLogMessageHandler
handlerEndpoint := ""
dummyEndpoint := ""
if hs.Cfg.GrafanaJavascriptAgent.Enabled {
rateLimiter = rate.NewLimiter(rate.Limit(hs.Cfg.GrafanaJavascriptAgent.EndpointRPS), hs.Cfg.GrafanaJavascriptAgent.EndpointBurst)
handler = GrafanaJavascriptAgentLogMessageHandler(sourceMapStore)
handlerEndpoint = grafanaJavascriptAgentEndpointPath
dummyEndpoint = sentryLogEndpointPath
} else {
rateLimiter = rate.NewLimiter(rate.Limit(hs.Cfg.Sentry.EndpointRPS), hs.Cfg.Sentry.EndpointBurst)
handler = NewFrontendLogMessageHandler(sourceMapStore)
handlerEndpoint = sentryLogEndpointPath
dummyEndpoint = grafanaJavascriptAgentEndpointPath
}
return func(ctx *web.Context) { return func(ctx *web.Context) {
if ctx.Req.Method == http.MethodPost && ctx.Req.URL.Path == dummyEndpoint { if ctx.Req.Method == http.MethodPost && ctx.Req.URL.Path == grafanaJavascriptAgentEndpointPath {
ctx.Resp.WriteHeader(http.StatusAccepted)
_, err := ctx.Resp.Write([]byte("OK"))
if err != nil {
hs.log.Error("could not write to response", "err", err)
}
}
if ctx.Req.Method == http.MethodPost && ctx.Req.URL.Path == handlerEndpoint {
if !rateLimiter.AllowN(time.Now(), 1) { if !rateLimiter.AllowN(time.Now(), 1) {
ctx.Resp.WriteHeader(http.StatusTooManyRequests) ctx.Resp.WriteHeader(http.StatusTooManyRequests)
return return

@ -9,7 +9,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/getsentry/sentry-go"
"github.com/go-kit/log" "github.com/go-kit/log"
"github.com/go-kit/log/level" "github.com/go-kit/log/level"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -30,79 +29,6 @@ type SourceMapReadRecord struct {
type logScenarioFunc func(c *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) type logScenarioFunc func(c *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord)
func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.FrontendSentryEvent, fn logScenarioFunc) {
t.Run(desc, func(t *testing.T) {
var logcontent = make(map[string]interface{})
logcontent["logger"] = "frontend"
newfrontendLogger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error {
for i := 0; i < len(keyvals); i += 2 {
logcontent[keyvals[i].(string)] = keyvals[i+1]
}
return nil
}))
origHandler := frontendLogger.GetLogger()
frontendLogger.Swap(level.NewFilter(newfrontendLogger, level.AllowInfo()))
sourceMapReads := []SourceMapReadRecord{}
t.Cleanup(func() {
frontendLogger.Swap(origHandler)
})
sc := setupScenarioContext(t, "/log")
cdnRootURL, e := url.Parse("https://storage.googleapis.com/grafana-static-assets")
require.NoError(t, e)
cfg := &setting.Cfg{
StaticRootPath: "/staticroot",
CDNRootURL: cdnRootURL,
}
readSourceMap := func(dir string, path string) ([]byte, error) {
sourceMapReads = append(sourceMapReads, SourceMapReadRecord{
dir: dir,
path: path,
})
if strings.Contains(path, "error") {
return nil, errors.New("epic hard drive failure")
}
if strings.HasSuffix(path, "foo.js.map") {
f, err := os.ReadFile("./frontendlogging/test-data/foo.js.map")
require.NoError(t, err)
return f, nil
}
return nil, os.ErrNotExist
}
// fake plugin route so we will try to find a source map there
pm := fakePluginStaticRouteResolver{
routes: []*plugins.StaticRoute{
{
Directory: "/usr/local/telepathic-panel",
PluginID: "telepathic",
},
},
}
sourceMapStore := frontendlogging.NewSourceMapStore(cfg, &pm, readSourceMap)
loggingHandler := NewFrontendLogMessageHandler(sourceMapStore)
handler := routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
sc.context = c
c.Req.Body = mockRequestBody(event)
c.Req.Header.Add("Content-Type", "application/json")
loggingHandler(nil, c.Context)
return response.Success("ok")
})
sc.m.Post(sc.url, handler)
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
fn(sc, logcontent, sourceMapReads)
})
}
func logGrafanaJavascriptAgentEventScenario(t *testing.T, desc string, event frontendlogging.FrontendGrafanaJavascriptAgentEvent, fn logScenarioFunc) { func logGrafanaJavascriptAgentEventScenario(t *testing.T, desc string, event frontendlogging.FrontendGrafanaJavascriptAgentEvent, fn logScenarioFunc) {
t.Run(desc, func(t *testing.T) { t.Run(desc, func(t *testing.T) {
var logcontent = make(map[string]interface{}) var logcontent = make(map[string]interface{})
@ -176,216 +102,6 @@ func logGrafanaJavascriptAgentEventScenario(t *testing.T, desc string, event fro
}) })
} }
func TestFrontendLoggingEndpointSentry(t *testing.T) {
ts, err := time.Parse("2006-01-02T15:04:05.000Z", "2020-10-22T06:29:29.078Z")
require.NoError(t, err)
t.Run("FrontendLoggingEndpoint", func(t *testing.T) {
request := sentry.Request{
URL: "http://localhost:3000/",
Headers: map[string]string{
"User-Agent": "Chrome",
},
}
user := sentry.User{
Email: "geralt@kaermorhen.com",
ID: "45",
}
event := sentry.Event{
EventID: "123",
Level: sentry.LevelError,
Request: &request,
Timestamp: ts,
}
errorEvent := frontendlogging.FrontendSentryEvent{
Event: &event,
Exception: &frontendlogging.FrontendSentryException{
Values: []frontendlogging.FrontendSentryExceptionValue{
{
Type: "UserError",
Value: "Please replace user and try again",
Stacktrace: sentry.Stacktrace{
Frames: []sentry.Frame{
{
Function: "foofn",
Filename: "foo.js",
Lineno: 123,
Colno: 23,
},
{
Function: "barfn",
Filename: "bar.js",
Lineno: 113,
Colno: 231,
},
},
},
},
},
},
}
logSentryEventScenario(t, "Should log received error event", errorEvent,
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, http.StatusAccepted, sc.resp.Code)
assertContextContains(t, logs, "logger", "frontend")
assertContextContains(t, logs, "url", errorEvent.Request.URL)
assertContextContains(t, logs, "user_agent", errorEvent.Request.Headers["User-Agent"])
assertContextContains(t, logs, "event_id", errorEvent.EventID)
assertContextContains(t, logs, "original_timestamp", errorEvent.Timestamp)
assertContextContains(t, logs, "stacktrace", `UserError: Please replace user and try again
at foofn (foo.js:123:23)
at barfn (bar.js:113:231)`)
assert.NotContains(t, logs, "context")
})
messageEvent := frontendlogging.FrontendSentryEvent{
Event: &sentry.Event{
EventID: "123",
Level: sentry.LevelInfo,
Request: &request,
Timestamp: ts,
Message: "hello world",
User: user,
},
Exception: nil,
}
logSentryEventScenario(t, "Should log received message event", messageEvent,
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, http.StatusAccepted, sc.resp.Code)
assert.Len(t, logs, 10)
assertContextContains(t, logs, "logger", "frontend")
assertContextContains(t, logs, "msg", "hello world")
assertContextContains(t, logs, level.Key().(string), level.InfoValue())
assertContextContains(t, logs, "logger", "frontend")
assertContextContains(t, logs, "url", messageEvent.Request.URL)
assertContextContains(t, logs, "user_agent", messageEvent.Request.Headers["User-Agent"])
assertContextContains(t, logs, "event_id", messageEvent.EventID)
assertContextContains(t, logs, "original_timestamp", messageEvent.Timestamp)
assert.NotContains(t, logs, "stacktrace")
assert.NotContains(t, logs, "context")
assertContextContains(t, logs, "user_email", user.Email)
assertContextContains(t, logs, "user_id", user.ID)
})
eventWithContext := frontendlogging.FrontendSentryEvent{
Event: &sentry.Event{
EventID: "123",
Level: sentry.LevelInfo,
Request: &request,
Timestamp: ts,
Message: "hello world",
User: user,
Contexts: map[string]interface{}{
"foo": map[string]interface{}{
"one": "two",
"three": 4,
},
"bar": "baz",
},
},
Exception: nil,
}
logSentryEventScenario(t, "Should log event context", eventWithContext,
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, http.StatusAccepted, sc.resp.Code)
assertContextContains(t, logs, "context_foo_one", "two")
assertContextContains(t, logs, "context_foo_three", "4")
assertContextContains(t, logs, "context_bar", "baz")
})
errorEventForSourceMapping := frontendlogging.FrontendSentryEvent{
Event: &event,
Exception: &frontendlogging.FrontendSentryException{
Values: []frontendlogging.FrontendSentryExceptionValue{
{
Type: "UserError",
Value: "Please replace user and try again",
Stacktrace: sentry.Stacktrace{
Frames: []sentry.Frame{
{
Function: "foofn",
Filename: "http://localhost:3000/public/build/moo/foo.js", // source map found and mapped, core
Lineno: 2,
Colno: 5,
},
{
Function: "foofn",
Filename: "http://localhost:3000/public/plugins/telepathic/foo.js", // plugin, source map found and mapped
Lineno: 3,
Colno: 10,
},
{
Function: "explode",
Filename: "http://localhost:3000/public/build/error.js", // reading source map throws error
Lineno: 3,
Colno: 10,
},
{
Function: "wat",
Filename: "http://localhost:3000/public/build/bar.js", // core, but source map not found on fs
Lineno: 3,
Colno: 10,
},
{
Function: "nope",
Filename: "http://localhost:3000/baz.js", // not core or plugin, wont even attempt to get source map
Lineno: 3,
Colno: 10,
},
{
Function: "fake",
Filename: "http://localhost:3000/public/build/../../secrets.txt", // path will be sanitized
Lineno: 3,
Colno: 10,
},
{
Function: "cdn",
Filename: "https://storage.googleapis.com/grafana-static-assets/grafana-oss/pre-releases/7.5.0-11925pre/public/build/foo.js", // source map found and mapped
Lineno: 3,
Colno: 10,
},
},
},
},
},
},
}
logSentryEventScenario(t, "Should load sourcemap and transform stacktrace line when possible",
errorEventForSourceMapping, func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, http.StatusAccepted, sc.resp.Code)
assert.Len(t, logs, 9)
assertContextContains(t, logs, "stacktrace", `UserError: Please replace user and try again
at ? (core|webpack:///./some_source.ts:2:2)
at ? (telepathic|webpack:///./some_source.ts:3:2)
at explode (http://localhost:3000/public/build/error.js:3:10)
at wat (http://localhost:3000/public/build/bar.js:3:10)
at nope (http://localhost:3000/baz.js:3:10)
at fake (http://localhost:3000/public/build/../../secrets.txt:3:10)
at ? (core|webpack:///./some_source.ts:3:2)`)
assert.Len(t, sourceMapReads, 6)
assert.Equal(t, "/staticroot", sourceMapReads[0].dir)
assert.Equal(t, "build/moo/foo.js.map", sourceMapReads[0].path)
assert.Equal(t, "/usr/local/telepathic-panel", sourceMapReads[1].dir)
assert.Equal(t, "/foo.js.map", sourceMapReads[1].path)
assert.Equal(t, "/staticroot", sourceMapReads[2].dir)
assert.Equal(t, "build/error.js.map", sourceMapReads[2].path)
assert.Equal(t, "/staticroot", sourceMapReads[3].dir)
assert.Equal(t, "build/bar.js.map", sourceMapReads[3].path)
assert.Equal(t, "/staticroot", sourceMapReads[4].dir)
assert.Equal(t, "secrets.txt.map", sourceMapReads[4].path)
assert.Equal(t, "/staticroot", sourceMapReads[5].dir)
assert.Equal(t, "build/foo.js.map", sourceMapReads[5].path)
})
})
}
func TestFrontendLoggingEndpointGrafanaJavascriptAgent(t *testing.T) { func TestFrontendLoggingEndpointGrafanaJavascriptAgent(t *testing.T) {
ts, err := time.Parse("2006-01-02T15:04:05.000Z", "2020-10-22T06:29:29.078Z") ts, err := time.Parse("2006-01-02T15:04:05.000Z", "2020-10-22T06:29:29.078Z")
require.NoError(t, err) require.NoError(t, err)
@ -536,13 +252,13 @@ func TestFrontendLoggingEndpointGrafanaJavascriptAgent(t *testing.T) {
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) { func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, http.StatusAccepted, sc.resp.Code) assert.Equal(t, http.StatusAccepted, sc.resp.Code)
assertContextContains(t, logs, "stacktrace", `UserError: Please replace user and try again assertContextContains(t, logs, "stacktrace", `UserError: Please replace user and try again
at ? (webpack:///./some_source.ts:2:2) at ? (core|webpack:///./some_source.ts:2:2)
at ? (webpack:///./some_source.ts:3:2) at ? (telepathic|webpack:///./some_source.ts:3:2)
at explode (http://localhost:3000/public/build/error.js:3:10) at explode (http://localhost:3000/public/build/error.js:3:10)
at wat (http://localhost:3000/public/build/bar.js:3:10) at wat (http://localhost:3000/public/build/bar.js:3:10)
at nope (http://localhost:3000/baz.js:3:10) at nope (http://localhost:3000/baz.js:3:10)
at fake (http://localhost:3000/public/build/../../secrets.txt:3:10) at fake (http://localhost:3000/public/build/../../secrets.txt:3:10)
at ? (webpack:///./some_source.ts:3:2)`) at ? (core|webpack:///./some_source.ts:3:2)`)
assert.Len(t, sourceMapReads, 6) assert.Len(t, sourceMapReads, 6)
assert.Equal(t, "/staticroot", sourceMapReads[0].dir) assert.Equal(t, "/staticroot", sourceMapReads[0].dir)
assert.Equal(t, "build/moo/foo.js.map", sourceMapReads[0].path) assert.Equal(t, "build/moo/foo.js.map", sourceMapReads[0].path)

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
) )
type CtxVector []interface{}
type FrontendGrafanaJavascriptAgentEvent struct { type FrontendGrafanaJavascriptAgentEvent struct {
Exceptions []Exception `json:"exceptions,omitempty"` Exceptions []Exception `json:"exceptions,omitempty"`
Logs []Log `json:"logs,omitempty"` Logs []Log `json:"logs,omitempty"`

@ -1,33 +1,5 @@
package frontendlogging package frontendlogging
// ResolveSourceLocation resolves minified source location to original source location
func ResolveSourceLocation(store *SourceMapStore, frame *Frame) (*Frame, error) {
smap, err := store.getSourceMap(frame.Filename)
if err != nil {
return nil, err
}
if smap == nil {
return nil, nil
}
file, function, line, col, ok := smap.consumer.Source(frame.Lineno, frame.Colno)
if !ok {
return nil, nil
}
// unfortunately in many cases go-sourcemap fails to determine the original function name.
// not a big issue as long as file, line and column are correct
if len(function) == 0 {
function = "?"
}
return &Frame{
Filename: file,
Lineno: line,
Colno: col,
Function: function,
}, nil
}
// TransformException will attempt to resolved all monified source locations in the stacktrace with original source locations // TransformException will attempt to resolved all monified source locations in the stacktrace with original source locations
func TransformException(ex *Exception, store *SourceMapStore) *Exception { func TransformException(ex *Exception, store *SourceMapStore) *Exception {
if ex.Stacktrace == nil { if ex.Stacktrace == nil {
@ -37,7 +9,7 @@ func TransformException(ex *Exception, store *SourceMapStore) *Exception {
for _, frame := range ex.Stacktrace.Frames { for _, frame := range ex.Stacktrace.Frames {
frame := frame frame := frame
mappedFrame, err := ResolveSourceLocation(store, &frame) mappedFrame, err := store.resolveSourceLocation(frame)
if err != nil { if err != nil {
frames = append(frames, frame) frames = append(frames, frame)
} else if mappedFrame != nil { } else if mappedFrame != nil {

@ -1,108 +0,0 @@
package frontendlogging
import (
"encoding/json"
"fmt"
"strings"
"github.com/getsentry/sentry-go"
"github.com/grafana/grafana/pkg/infra/log"
)
type CtxVector []interface{}
var logger = log.New("frontendlogging")
type FrontendSentryExceptionValue struct {
Value string `json:"value,omitempty"`
Type string `json:"type,omitempty"`
Stacktrace sentry.Stacktrace `json:"stacktrace,omitempty"`
}
type FrontendSentryException struct {
Values []FrontendSentryExceptionValue `json:"values,omitempty"`
}
type FrontendSentryEvent struct {
*sentry.Event
Exception *FrontendSentryException `json:"exception,omitempty"`
}
func (value *FrontendSentryExceptionValue) FmtMessage() string {
return fmt.Sprintf("%s: %s", value.Type, value.Value)
}
func fmtLine(frame sentry.Frame) string {
module := ""
if len(frame.Module) > 0 {
module = frame.Module + "|"
}
return fmt.Sprintf("\n at %s (%s%s:%v:%v)", frame.Function, module, frame.Filename, frame.Lineno, frame.Colno)
}
func (value *FrontendSentryExceptionValue) FmtStacktrace(store *SourceMapStore) string {
var stacktrace = value.FmtMessage()
for _, frame := range value.Stacktrace.Frames {
mappedFrame, err := store.resolveSourceLocation(frame)
if err != nil {
logger.Error("Error resolving stack trace frame source location", "err", err)
stacktrace += fmtLine(frame) // even if reading source map fails for unexpected reason, still better to log compiled location than nothing at all
} else {
if mappedFrame != nil {
stacktrace += fmtLine(*mappedFrame)
} else {
stacktrace += fmtLine(frame)
}
}
}
return stacktrace
}
func (exception *FrontendSentryException) FmtStacktraces(store *SourceMapStore) string {
stacktraces := make([]string, 0, len(exception.Values))
for _, value := range exception.Values {
stacktraces = append(stacktraces, value.FmtStacktrace(store))
}
return strings.Join(stacktraces, "\n\n")
}
func addEventContextToLogContext(rootPrefix string, logCtx *CtxVector, eventCtx map[string]interface{}) {
for key, element := range eventCtx {
prefix := fmt.Sprintf("%s_%s", rootPrefix, key)
switch v := element.(type) {
case map[string]interface{}:
addEventContextToLogContext(prefix, logCtx, v)
default:
*logCtx = append(*logCtx, prefix, fmt.Sprintf("%v", v))
}
}
}
func (event *FrontendSentryEvent) ToLogContext(store *SourceMapStore) []interface{} {
var ctx = CtxVector{"url", event.Request.URL, "user_agent", event.Request.Headers["User-Agent"],
"event_id", event.EventID, "original_timestamp", event.Timestamp}
if event.Exception != nil {
ctx = append(ctx, "stacktrace", event.Exception.FmtStacktraces(store))
}
addEventContextToLogContext("context", &ctx, event.Contexts)
if len(event.User.Email) > 0 {
ctx = append(ctx, "user_email", event.User.Email, "user_id", event.User.ID)
}
return ctx
}
func (event *FrontendSentryEvent) MarshalJSON() ([]byte, error) {
eventJSON, err := json.Marshal(event.Event)
if err != nil {
return nil, err
}
exceptionJSON, err := json.Marshal(map[string]interface{}{"exception": event.Exception})
if err != nil {
return nil, err
}
exceptionJSON[0] = ','
return append(eventJSON[:len(eventJSON)-1], exceptionJSON...), nil
}

@ -9,13 +9,14 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/getsentry/sentry-go"
sourcemap "github.com/go-sourcemap/sourcemap" sourcemap "github.com/go-sourcemap/sourcemap"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
var logger = log.New("frontendlogging")
type sourceMapLocation struct { type sourceMapLocation struct {
dir string dir string
path string path string
@ -136,7 +137,7 @@ func (store *SourceMapStore) getSourceMap(sourceURL string) (*sourceMap, error)
return smap, nil return smap, nil
} }
func (store *SourceMapStore) resolveSourceLocation(frame sentry.Frame) (*sentry.Frame, error) { func (store *SourceMapStore) resolveSourceLocation(frame Frame) (*Frame, error) {
smap, err := store.getSourceMap(frame.Filename) smap, err := store.getSourceMap(frame.Filename)
if err != nil { if err != nil {
return nil, err return nil, err
@ -157,7 +158,7 @@ func (store *SourceMapStore) resolveSourceLocation(frame sentry.Frame) (*sentry.
if len(smap.pluginID) > 0 { if len(smap.pluginID) > 0 {
module = smap.pluginID module = smap.pluginID
} }
return &sentry.Frame{ return &Frame{
Filename: file, Filename: file,
Lineno: line, Lineno: line,
Colno: col, Colno: col,

@ -184,7 +184,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
RendererVersion: hs.RenderService.Version(), RendererVersion: hs.RenderService.Version(),
SecretsManagerPluginEnabled: secretsManagerPluginEnabled, SecretsManagerPluginEnabled: secretsManagerPluginEnabled,
Http2Enabled: hs.Cfg.Protocol == setting.HTTP2Scheme, Http2Enabled: hs.Cfg.Protocol == setting.HTTP2Scheme,
Sentry: hs.Cfg.Sentry,
GrafanaJavascriptAgent: hs.Cfg.GrafanaJavascriptAgent, GrafanaJavascriptAgent: hs.Cfg.GrafanaJavascriptAgent,
PluginCatalogURL: hs.Cfg.PluginCatalogURL, PluginCatalogURL: hs.Cfg.PluginCatalogURL,
PluginAdminEnabled: hs.Cfg.PluginAdminEnabled, PluginAdminEnabled: hs.Cfg.PluginAdminEnabled,

@ -136,7 +136,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
AppleTouchIcon: "public/img/apple-touch-icon.png", AppleTouchIcon: "public/img/apple-touch-icon.png",
AppTitle: "Grafana", AppTitle: "Grafana",
NavTree: navTree, NavTree: navTree,
Sentry: &hs.Cfg.Sentry,
Nonce: c.RequestNonce, Nonce: c.RequestNonce,
ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()), ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()),
LoadingLogo: "public/img/grafana_icon.svg", LoadingLogo: "public/img/grafana_icon.svg",

@ -363,9 +363,6 @@ type Cfg struct {
DashboardAnnotationCleanupSettings AnnotationCleanupSettings DashboardAnnotationCleanupSettings AnnotationCleanupSettings
APIAnnotationCleanupSettings AnnotationCleanupSettings APIAnnotationCleanupSettings AnnotationCleanupSettings
// Sentry config
Sentry Sentry
// GrafanaJavascriptAgent config // GrafanaJavascriptAgent config
GrafanaJavascriptAgent GrafanaJavascriptAgent GrafanaJavascriptAgent GrafanaJavascriptAgent
@ -1187,7 +1184,6 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
cfg.GeomapEnableCustomBaseLayers = geomapSection.Key("enable_custom_baselayers").MustBool(true) cfg.GeomapEnableCustomBaseLayers = geomapSection.Key("enable_custom_baselayers").MustBool(true)
cfg.readDateFormats() cfg.readDateFormats()
cfg.readSentryConfig()
cfg.readGrafanaJavascriptAgentConfig() cfg.readGrafanaJavascriptAgentConfig()
if err := cfg.readLiveSettings(iniFile); err != nil { if err := cfg.readLiveSettings(iniFile); err != nil {

@ -13,17 +13,14 @@ type GrafanaJavascriptAgent struct {
func (cfg *Cfg) readGrafanaJavascriptAgentConfig() { func (cfg *Cfg) readGrafanaJavascriptAgentConfig() {
raw := cfg.Raw.Section("log.frontend") raw := cfg.Raw.Section("log.frontend")
provider := raw.Key("provider").MustString("sentry") cfg.GrafanaJavascriptAgent = GrafanaJavascriptAgent{
if provider == "grafana" { Enabled: raw.Key("enabled").MustBool(true),
cfg.GrafanaJavascriptAgent = GrafanaJavascriptAgent{ CustomEndpoint: raw.Key("custom_endpoint").MustString("/log-grafana-javascript-agent"),
Enabled: raw.Key("enabled").MustBool(true), EndpointRPS: raw.Key("log_endpoint_requests_per_second_limit").MustInt(3),
CustomEndpoint: raw.Key("custom_endpoint").MustString("/log-grafana-javascript-agent"), EndpointBurst: raw.Key("log_endpoint_burst_limit").MustInt(15),
EndpointRPS: raw.Key("log_endpoint_requests_per_second_limit").MustInt(3), ErrorInstrumentalizationEnabled: raw.Key("instrumentations_errors_enabled").MustBool(true),
EndpointBurst: raw.Key("log_endpoint_burst_limit").MustInt(15), ConsoleInstrumentalizationEnabled: raw.Key("instrumentations_console_enabled").MustBool(true),
ErrorInstrumentalizationEnabled: raw.Key("instrumentations_errors_enabled").MustBool(true), WebVitalsInstrumentalizationEnabled: raw.Key("instrumentations_webvitals_enabled").MustBool(true),
ConsoleInstrumentalizationEnabled: raw.Key("instrumentations_console_enabled").MustBool(true), ApiKey: raw.Key("api_key").String(),
WebVitalsInstrumentalizationEnabled: raw.Key("instrumentations_webvitals_enabled").MustBool(true),
ApiKey: raw.Key("api_key").String(),
}
} }
} }

@ -1,28 +0,0 @@
package setting
import "github.com/go-kit/log/level"
type Sentry struct {
Enabled bool `json:"enabled"`
DSN string `json:"dsn"`
CustomEndpoint string `json:"customEndpoint"`
SampleRate float64 `json:"sampleRate"`
EndpointRPS int `json:"-"`
EndpointBurst int `json:"-"`
}
func (cfg *Cfg) readSentryConfig() {
raw := cfg.Raw.Section("log.frontend")
provider := raw.Key("provider").MustString("sentry")
if provider == "sentry" || provider != "grafana" {
_ = level.Warn(cfg.Logger).Log("msg", "\"sentry\" frontend logging provider is deprecated and will be removed in the next major version. Use \"grafana\" provider instead.")
cfg.Sentry = Sentry{
Enabled: raw.Key("enabled").MustBool(true),
DSN: raw.Key("sentry_dsn").String(),
CustomEndpoint: raw.Key("custom_endpoint").MustString("/log"),
SampleRate: raw.Key("sample_rate").MustFloat64(),
EndpointRPS: raw.Key("log_endpoint_requests_per_second_limit").MustInt(3),
EndpointBurst: raw.Key("log_endpoint_burst_limit").MustInt(15),
}
}
}

@ -66,7 +66,6 @@ import { GA4EchoBackend } from './core/services/echo/backends/analytics/GA4Backe
import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend'; import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend';
import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend'; import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend';
import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend'; import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend';
import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend';
import { KeybindingSrv } from './core/services/keybindingSrv'; import { KeybindingSrv } from './core/services/keybindingSrv';
import { initDevFeatures } from './dev'; import { initDevFeatures } from './dev';
import { getTimeSrv } from './features/dashboard/services/TimeSrv'; import { getTimeSrv } from './features/dashboard/services/TimeSrv';
@ -259,15 +258,6 @@ function initEchoSrv() {
registerEchoBackend(new PerformanceBackend({})); registerEchoBackend(new PerformanceBackend({}));
} }
if (config.sentry.enabled) {
registerEchoBackend(
new SentryEchoBackend({
...config.sentry,
user: config.bootData.user,
buildInfo: config.buildInfo,
})
);
}
if (config.grafanaJavascriptAgent.enabled) { if (config.grafanaJavascriptAgent.enabled) {
registerEchoBackend( registerEchoBackend(
new GrafanaJavascriptAgentBackend({ new GrafanaJavascriptAgentBackend({

@ -1,5 +1,4 @@
import { captureException } from '@sentry/browser'; import { faro } from '@grafana/faro-web-sdk';
import { getEchoSrv, EchoEventType } from '@grafana/runtime'; import { getEchoSrv, EchoEventType } from '@grafana/runtime';
import { PerformanceEvent } from './backends/PerformanceBackend'; import { PerformanceEvent } from './backends/PerformanceBackend';
@ -14,6 +13,5 @@ export const reportPerformance = (metric: string, value: number) => {
}); });
}; };
// Sentry will process the error, adding its own metadata, applying any sampling rules, // Farp will process the error, then push it to EchoSrv as GrafanaJavascriptAgent event
// then push it to EchoSrv as SentryEvent export const reportError = (error: Error) => faro?.api?.pushError(error);
export const reportError = (error: Error) => captureException(error);

@ -1,143 +0,0 @@
import { init as initSentry, setUser as sentrySetUser, Event as SentryEvent } from '@sentry/browser';
import { FetchTransport } from '@sentry/browser/dist/transports';
import { waitFor } from '@testing-library/react';
import { BuildInfo } from '@grafana/data';
import { GrafanaEdition } from '@grafana/data/src/types/config';
import { EchoBackend, EchoEventType, EchoMeta, setEchoSrv } from '@grafana/runtime';
import { Echo } from '../../Echo';
import { SentryEchoBackend, SentryEchoBackendOptions } from './SentryBackend';
import { CustomEndpointTransport } from './transports/CustomEndpointTransport';
import { EchoSrvTransport } from './transports/EchoSrvTransport';
import { SentryEchoEvent } from './types';
jest.mock('@sentry/browser');
describe('SentryEchoBackend', () => {
beforeEach(() => {
jest.resetAllMocks();
window.fetch = jest.fn();
});
const buildInfo: BuildInfo = {
version: '1.0',
commit: 'abcd123',
env: 'production',
edition: GrafanaEdition.OpenSource,
latestVersion: 'ba',
hasUpdate: false,
hideVersion: false,
};
const options: SentryEchoBackendOptions = {
enabled: true,
buildInfo,
dsn: 'https://examplePublicKey@o0.ingest.testsentry.io/0',
sampleRate: 1,
customEndpoint: '',
user: {
email: 'darth.vader@sith.glx',
id: 504,
orgId: 1,
},
};
it('will set up sentry`s FetchTransport if DSN is provided', async () => {
const backend = new SentryEchoBackend(options);
expect(backend.transports.length).toEqual(1);
expect(backend.transports[0]).toBeInstanceOf(FetchTransport);
expect((backend.transports[0] as FetchTransport).options.dsn).toEqual(options.dsn);
});
it('will set up custom endpoint transport if custom endpoint is provided', async () => {
const backend = new SentryEchoBackend({
...options,
dsn: '',
customEndpoint: '/log',
});
expect(backend.transports.length).toEqual(1);
expect(backend.transports[0]).toBeInstanceOf(CustomEndpointTransport);
expect((backend.transports[0] as CustomEndpointTransport).options.endpoint).toEqual('/log');
});
it('will initialize sentry and set user', async () => {
new SentryEchoBackend(options);
expect(initSentry).toHaveBeenCalledTimes(1);
expect(initSentry).toHaveBeenCalledWith({
release: buildInfo.version,
environment: buildInfo.env,
dsn: options.dsn,
sampleRate: options.sampleRate,
transport: EchoSrvTransport,
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'ResizeObserver loop completed',
'Non-Error exception captured with keys',
],
});
expect(sentrySetUser).toHaveBeenCalledWith({
email: options.user?.email,
id: String(options.user?.id),
});
});
it('will forward events to transports', async () => {
const backend = new SentryEchoBackend(options);
backend.transports = [{ sendEvent: jest.fn() }, { sendEvent: jest.fn() }];
const event: SentryEchoEvent = {
type: EchoEventType.Sentry,
payload: { foo: 'bar' } as unknown as SentryEvent,
meta: {} as unknown as EchoMeta,
};
backend.addEvent(event);
backend.transports.forEach((transport) => {
expect(transport.sendEvent).toHaveBeenCalledTimes(1);
expect(transport.sendEvent).toHaveBeenCalledWith(event.payload);
});
});
it('integration test with EchoSrv, Sentry and CustomFetchTransport', async () => {
// sets up the whole thing between window.onerror and backend endpoint call, checks that error is reported
// use actual sentry & mock window.fetch
const sentry = jest.requireActual('@sentry/browser');
(initSentry as jest.Mock).mockImplementation(sentry.init);
(sentrySetUser as jest.Mock).mockImplementation(sentry.setUser);
const fetchSpy = (window.fetch = jest.fn());
fetchSpy.mockResolvedValue({ status: 200 } as Response);
// set up echo srv & sentry backend
const echo = new Echo({ debug: true });
setEchoSrv(echo);
const sentryBackend = new SentryEchoBackend({
...options,
dsn: '',
customEndpoint: '/log',
});
echo.addBackend(sentryBackend);
// lets add another echo backend for sentry events for good measure
const myCustomErrorBackend: EchoBackend = {
supportedEvents: [EchoEventType.Sentry],
flush: () => {},
options: {},
addEvent: jest.fn(),
};
echo.addBackend(myCustomErrorBackend);
// fire off an error using global error handler, Sentry should pick it up
const error = new Error('test error');
window.onerror!(error.message, undefined, undefined, undefined, error);
// check that error was reported to backend
await waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(1));
const [url, reqInit]: [string, RequestInit] = fetchSpy.mock.calls[0];
expect(url).toEqual('/log');
expect((JSON.parse(reqInit.body as string) as SentryEvent).exception!.values![0].value).toEqual('test error');
// check that our custom backend got it too
expect(myCustomErrorBackend.addEvent).toHaveBeenCalledTimes(1);
});
});

@ -1,66 +0,0 @@
import { BrowserOptions, init as initSentry, setUser as sentrySetUser } from '@sentry/browser';
import { FetchTransport } from '@sentry/browser/dist/transports';
import { BuildInfo } from '@grafana/data';
import { SentryConfig } from '@grafana/data/src/types/config';
import { EchoBackend, EchoEventType } from '@grafana/runtime';
import { CustomEndpointTransport } from './transports/CustomEndpointTransport';
import { EchoSrvTransport } from './transports/EchoSrvTransport';
import { SentryEchoEvent, User, BaseTransport } from './types';
export interface SentryEchoBackendOptions extends SentryConfig {
user?: User;
buildInfo: BuildInfo;
}
export class SentryEchoBackend implements EchoBackend<SentryEchoEvent, SentryEchoBackendOptions> {
supportedEvents = [EchoEventType.Sentry];
transports: BaseTransport[];
constructor(public options: SentryEchoBackendOptions) {
// set up transports to post events to grafana backend and/or Sentry
this.transports = [];
if (options.dsn) {
this.transports.push(new FetchTransport({ dsn: options.dsn }, fetch));
}
if (options.customEndpoint) {
this.transports.push(new CustomEndpointTransport({ endpoint: options.customEndpoint }));
}
// initialize Sentry so it can set up its hooks and start collecting errors
const sentryOptions: BrowserOptions = {
release: options.buildInfo.version,
environment: options.buildInfo.env,
// seems Sentry won't attempt to send events to transport unless a valid DSN is defined :shrug:
dsn: options.dsn || 'https://examplePublicKey@o0.ingest.sentry.io/0',
sampleRate: options.sampleRate,
transport: EchoSrvTransport, // will dump errors to EchoSrv
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'ResizeObserver loop completed',
'Non-Error exception captured with keys',
],
};
if (options.user) {
sentrySetUser({
email: options.user.email,
id: String(options.user.id),
});
}
initSentry(sentryOptions);
}
addEvent = (e: SentryEchoEvent) => {
this.transports.forEach((t) => t.sendEvent(e.payload));
};
// backend will log events to stdout, and at least in case of hosted grafana they will be
// ingested into Loki. Due to Loki limitations logs cannot be backdated,
// so not using buffering for this backend to make sure that events are logged as close
// to their context as possible
flush = () => {};
}

@ -1,133 +0,0 @@
import { Event, Severity } from '@sentry/browser';
import { CustomEndpointTransport } from './CustomEndpointTransport';
describe('CustomEndpointTransport', () => {
const fetchSpy = (window.fetch = jest.fn());
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
jest.resetAllMocks();
// The code logs a warning to console
// Let's stub this out so we don't pollute the test output
consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
consoleSpy.mockRestore();
});
const now = new Date();
const event: Event = {
level: Severity.Error,
breadcrumbs: [],
exception: {
values: [
{
type: 'SomeError',
value: 'foo',
},
],
},
timestamp: now.getTime() / 1000,
};
it('will send received event to backend using window.fetch', async () => {
fetchSpy.mockResolvedValue({ status: 200 });
const transport = new CustomEndpointTransport({ endpoint: '/log' });
await transport.sendEvent(event);
expect(fetchSpy).toHaveBeenCalledTimes(1);
const [url, reqInit]: [string, RequestInit] = fetchSpy.mock.calls[0];
expect(url).toEqual('/log');
expect(reqInit.method).toEqual('POST');
expect(reqInit.headers).toEqual({
'Content-Type': 'application/json',
});
expect(JSON.parse(reqInit.body!.toString())).toEqual({
...event,
timestamp: now.toISOString(),
});
});
it('will back off if backend returns Retry-After', async () => {
const rateLimiterResponse = {
status: 429,
ok: false,
headers: new Headers({
'Retry-After': '1', // 1 second
}),
} as Response;
fetchSpy.mockResolvedValueOnce(rateLimiterResponse).mockResolvedValueOnce({ status: 200 });
const transport = new CustomEndpointTransport({ endpoint: '/log' });
// first call - backend is called, rejected because of 429
await expect(transport.sendEvent(event)).rejects.toEqual(rateLimiterResponse);
expect(fetchSpy).toHaveBeenCalledTimes(1);
// second immediate call - shot circuited because retry-after time has not expired, backend not called
await expect(transport.sendEvent(event)).resolves.toHaveProperty('status', 'skipped');
expect(fetchSpy).toHaveBeenCalledTimes(1);
// wait out the retry-after and call again - great success
await new Promise((resolve) => setTimeout(() => resolve(null), 1001));
await expect(transport.sendEvent(event)).resolves.toBeTruthy();
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('will back off if backend returns Retry-After', async () => {
const rateLimiterResponse = {
status: 429,
ok: false,
headers: new Headers({
'Retry-After': '1', // 1 second
}),
} as Response;
fetchSpy.mockResolvedValueOnce(rateLimiterResponse).mockResolvedValueOnce({ status: 200 });
const transport = new CustomEndpointTransport({ endpoint: '/log' });
// first call - backend is called, rejected because of 429
await expect(transport.sendEvent(event)).rejects.toHaveProperty('status', 429);
expect(fetchSpy).toHaveBeenCalledTimes(1);
// second immediate call - shot circuited because retry-after time has not expired, backend not called
await expect(transport.sendEvent(event)).resolves.toHaveProperty('status', 'skipped');
expect(fetchSpy).toHaveBeenCalledTimes(1);
// wait out the retry-after and call again - great success
await new Promise((resolve) => setTimeout(() => resolve(null), 1001));
await expect(transport.sendEvent(event)).resolves.toBeTruthy();
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('will drop events and log a warning to console if max concurrency is reached', async () => {
const calls: Array<(value: unknown) => void> = [];
fetchSpy.mockImplementation(
() =>
new Promise((resolve) => {
calls.push(resolve);
})
);
const transport = new CustomEndpointTransport({ endpoint: '/log', maxConcurrentRequests: 2 });
// first two requests are accepted
transport.sendEvent(event);
const event2 = transport.sendEvent(event);
expect(calls).toHaveLength(2);
// third is skipped because too many requests in flight
await expect(transport.sendEvent(event)).resolves.toHaveProperty('status', 'skipped');
expect(calls).toHaveLength(2);
// after resolving in flight requests, next request is accepted as well
calls.forEach((call) => {
call({ status: 200 });
});
await event2;
const event3 = transport.sendEvent(event);
expect(calls).toHaveLength(3);
calls[2]({ status: 200 });
await event3;
});
});

@ -1,151 +0,0 @@
import { Event, Severity } from '@sentry/browser';
import { Response } from '@sentry/types';
import {
logger,
makePromiseBuffer,
parseRetryAfterHeader,
PromiseBuffer,
supportsReferrerPolicy,
SyncPromise,
} from '@sentry/utils';
import { BaseTransport } from '../types';
export interface CustomEndpointTransportOptions {
endpoint: string;
fetchParameters?: Partial<RequestInit>;
maxConcurrentRequests?: number;
}
const DEFAULT_MAX_CONCURRENT_REQUESTS = 3;
const DEFAULT_RATE_LIMIT_TIMEOUT_MS = 5000;
/**
* This is a copy of sentry's FetchTransport, edited to be able to push to any custom url
* instead of using Sentry-specific endpoint logic.
* Also transforms some of the payload values to be parseable by go.
* Sends events sequentially and implements back-off in case of rate limiting.
*/
export class CustomEndpointTransport implements BaseTransport {
/** Locks transport after receiving 429 response */
private _disabledUntil: Date = new Date(Date.now());
private readonly _buffer: PromiseBuffer<Response>;
constructor(public options: CustomEndpointTransportOptions) {
this._buffer = makePromiseBuffer(options.maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS);
}
sendEvent(event: Event): PromiseLike<Response> {
if (new Date(Date.now()) < this._disabledUntil) {
const reason = `Dropping frontend event due to too many requests.`;
console.warn(reason);
return Promise.resolve({
event,
reason,
status: 'skipped',
});
}
const sentryReq = {
// convert all timestamps to iso string, so it's parseable by backend
body: JSON.stringify({
...event,
level: event.level ?? (event.exception ? Severity.Error : Severity.Info),
exception: event.exception
? {
values: event.exception.values?.map((value) => ({
...value,
// according to both typescript and go types, value is supposed to be string.
// but in some odd cases at runtime it turns out to be an empty object {}
// let's fix it here
value: fmtSentryErrorValue(value.value),
})),
}
: event.exception,
breadcrumbs: event.breadcrumbs?.map((breadcrumb) => ({
...breadcrumb,
timestamp: makeTimestamp(breadcrumb.timestamp),
})),
timestamp: makeTimestamp(event.timestamp),
}),
url: this.options.endpoint,
};
const options: RequestInit = {
body: sentryReq.body,
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
// https://caniuse.com/#feat=referrer-policy
// It doesn't. And it throw exception instead of ignoring this parameter...
// REF: https://github.com/getsentry/raven-js/issues/1233
referrerPolicy: supportsReferrerPolicy() ? 'origin' : '',
};
if (this.options.fetchParameters !== undefined) {
Object.assign(options, this.options.fetchParameters);
}
return this._buffer
.add(
() =>
new SyncPromise<Response>((resolve, reject) => {
window
.fetch(sentryReq.url, options)
.then((response) => {
if (response.status === 200) {
resolve({ status: 'success' });
return;
}
if (response.status === 429) {
const now = Date.now();
const retryAfterHeader = response.headers.get('Retry-After');
if (retryAfterHeader) {
this._disabledUntil = new Date(now + parseRetryAfterHeader(retryAfterHeader, now));
} else {
this._disabledUntil = new Date(now + DEFAULT_RATE_LIMIT_TIMEOUT_MS);
}
logger.warn(`Too many requests, backing off till: ${this._disabledUntil}`);
}
reject(response);
})
.catch(reject);
})
)
.then(undefined, (reason) => {
if (reason.message === 'Not adding Promise due to buffer limit reached.') {
const msg = `Dropping frontend log event due to too many requests in flight.`;
console.warn(msg);
return {
event,
reason: msg,
status: 'skipped',
};
}
throw reason;
});
}
}
function makeTimestamp(time: number | undefined): string {
if (time) {
return new Date(time * 1000).toISOString();
}
return new Date().toISOString();
}
function fmtSentryErrorValue(value: unknown): string | undefined {
if (typeof value === 'string' || value === undefined) {
return value;
} else if (value && typeof value === 'object' && Object.keys(value).length === 0) {
return '';
}
return String(value);
}

@ -1,26 +0,0 @@
import { Event } from '@sentry/browser';
import { BaseTransport } from '@sentry/browser/dist/transports';
import { EventStatus, Request, Session, Response } from '@sentry/types';
import { getEchoSrv, EchoEventType } from '@grafana/runtime';
export class EchoSrvTransport extends BaseTransport {
sendEvent(event: Event): Promise<{ status: EventStatus; event: Event }> {
getEchoSrv().addEvent({
type: EchoEventType.Sentry,
payload: event,
});
return Promise.resolve({
status: 'success',
event,
});
}
// not recording sessions for now
sendSession(session: Session): PromiseLike<Response> {
return Promise.resolve({ status: 'skipped' });
}
// required by BaseTransport definition but not used by this implementation
_sendRequest(sentryRequest: Request, originalPayload: Event | Session): PromiseLike<Response> {
throw new Error('should not happen');
}
}

@ -1,16 +0,0 @@
import { Event as SentryEvent } from '@sentry/browser';
import { Response } from '@sentry/types';
import { EchoEvent, EchoEventType } from '@grafana/runtime';
export interface BaseTransport {
sendEvent(event: SentryEvent): PromiseLike<Response>;
}
export type SentryEchoEvent = EchoEvent<EchoEventType.Sentry, SentryEvent>;
export interface User {
email: string;
id: number;
orgId: number;
}

@ -17,7 +17,7 @@ export const LogMessages = {
unknownMessageFromError: 'unknown messageFromError', unknownMessageFromError: 'unknown messageFromError',
}; };
// logInfo from '@grafana/runtime' should be used, but it doesn't handle Grafana JS Agent and Sentry correctly // logInfo from '@grafana/runtime' should be used, but it doesn't handle Grafana JS Agent correctly
export function logInfo(message: string, context: Record<string, string | number> = {}) { export function logInfo(message: string, context: Record<string, string | number> = {}) {
if (config.grafanaJavascriptAgent.enabled) { if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushLog([message], { faro.api.pushLog([message], {

@ -3028,7 +3028,6 @@ __metadata:
"@grafana/ui": 10.1.0-pre "@grafana/ui": 10.1.0-pre
"@rollup/plugin-commonjs": 23.0.2 "@rollup/plugin-commonjs": 23.0.2
"@rollup/plugin-node-resolve": 15.0.1 "@rollup/plugin-node-resolve": 15.0.1
"@sentry/browser": 6.19.7
"@testing-library/dom": 9.0.1 "@testing-library/dom": 9.0.1
"@testing-library/react": 14.0.0 "@testing-library/react": 14.0.0
"@testing-library/user-event": 14.4.3 "@testing-library/user-event": 14.4.3
@ -3141,7 +3140,6 @@ __metadata:
"@react-aria/utils": 3.13.1 "@react-aria/utils": 3.13.1
"@react-stately/menu": 3.4.1 "@react-stately/menu": 3.4.1
"@rollup/plugin-node-resolve": 15.0.1 "@rollup/plugin-node-resolve": 15.0.1
"@sentry/browser": 6.19.7
"@storybook/addon-a11y": 6.5.16 "@storybook/addon-a11y": 6.5.16
"@storybook/addon-actions": 6.5.16 "@storybook/addon-actions": 6.5.16
"@storybook/addon-docs": 6.5.16 "@storybook/addon-docs": 6.5.16
@ -6451,70 +6449,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@sentry/browser@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/browser@npm:6.19.7"
dependencies:
"@sentry/core": 6.19.7
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
tslib: ^1.9.3
checksum: 071d00c76c2d0384580474c634c58c6196bbd1a3cf510da1309bd1565c57df7422fca8ceb717db189fa557f2c711a21664ee1ab935dfd9869faf416d388e6f78
languageName: node
linkType: hard
"@sentry/core@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/core@npm:6.19.7"
dependencies:
"@sentry/hub": 6.19.7
"@sentry/minimal": 6.19.7
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
tslib: ^1.9.3
checksum: d212e8ef07114549de4a93b81f8bfa217ca1550ca7a5eeaa611e5629faef78ff72663ce561ffa2cff48f3dc556745ef65177044f9965cdd3cbccf617cf3bf675
languageName: node
linkType: hard
"@sentry/hub@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/hub@npm:6.19.7"
dependencies:
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
tslib: ^1.9.3
checksum: 10bb1c5cba1b0f1e27a3dd0a186c22f94aeaf11c4662890ab07b2774f46f46af78d61e3ba71d76edc750a7b45af86edd032f35efecdb4efa2eaf551080ccdcb1
languageName: node
linkType: hard
"@sentry/minimal@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/minimal@npm:6.19.7"
dependencies:
"@sentry/hub": 6.19.7
"@sentry/types": 6.19.7
tslib: ^1.9.3
checksum: 9153ac426ee056fc34c5be898f83d74ec08f559d69f544c5944ec05e584b62ed356b92d1a9b08993a7022ad42b5661c3d72881221adc19bee5fc1af3ad3864a8
languageName: node
linkType: hard
"@sentry/types@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/types@npm:6.19.7"
checksum: f46ef74a33376ad6ea9b128115515c58eb9369d89293c60aa67abca26b5d5d519aa4d0a736db56ae0d75ffd816643d62187018298523cbc2e6c2fb3a6b2a9035
languageName: node
linkType: hard
"@sentry/utils@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/utils@npm:6.19.7"
dependencies:
"@sentry/types": 6.19.7
tslib: ^1.9.3
checksum: a000223b9c646c64e3565e79cace1eeb75114342b768367c4dddd646476c215eb1bddfb70c63f05e2352d3bce2d7d415344e4757a001605d0e01ac74da5dd306
languageName: node
linkType: hard
"@sideway/address@npm:^4.1.3": "@sideway/address@npm:^4.1.3":
version: 4.1.4 version: 4.1.4
resolution: "@sideway/address@npm:4.1.4" resolution: "@sideway/address@npm:4.1.4"
@ -18770,9 +18704,6 @@ __metadata:
"@reduxjs/toolkit": 1.9.3 "@reduxjs/toolkit": 1.9.3
"@remix-run/router": ^1.5.0 "@remix-run/router": ^1.5.0
"@rtsao/plugin-proposal-class-properties": 7.0.1-patch.1 "@rtsao/plugin-proposal-class-properties": 7.0.1-patch.1
"@sentry/browser": 6.19.7
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
"@swc/core": 1.3.38 "@swc/core": 1.3.38
"@swc/helpers": 0.4.14 "@swc/helpers": 0.4.14
"@testing-library/dom": 9.0.1 "@testing-library/dom": 9.0.1
@ -32939,7 +32870,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^1.10.0, tslib@npm:^1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.3": "tslib@npm:^1.10.0, tslib@npm:^1.14.1, tslib@npm:^1.8.1":
version: 1.14.1 version: 1.14.1
resolution: "tslib@npm:1.14.1" resolution: "tslib@npm:1.14.1"
checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd

Loading…
Cancel
Save