mirror of https://github.com/grafana/grafana
Frontend logging: Remove Sentry javascript agent support (#67493)
* remove Sentry * fix sourcemap resolvepull/67646/head
parent
9614dc2446
commit
15d4169813
@ -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 |
||||
} |
@ -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), |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
Loading…
Reference in new issue