Skip to content

Commit

Permalink
fix: check for telemetry consent before error reporting (#7019)
Browse files Browse the repository at this point in the history
* feat: check for telemetry consent before error reporting

* refactor: try approach with custom transport instead

* fixup!: await in flushBuffer and type check / invoke transport

* fixup!: type transport in sentry reporter and comments in buffered transport

Co-authored-by: Espen Hovlandsdal <espen@hovlandsdal.com>

* fixup!: reduce errorReporting enablement and cleanup sentry files

* chore: update pnpm from next

* fix: add back browseroptions cast

---------

Co-authored-by: Espen Hovlandsdal <espen@hovlandsdal.com>
  • Loading branch information
2 people authored and ricokahler committed Jun 27, 2024
1 parent 666c420 commit 83e1111
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 7 deletions.
1 change: 1 addition & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
"@sanity/pkg-utils": "6.9.3",
"@sanity/tsdoc": "1.0.72",
"@sanity/ui-workshop": "^1.2.11",
"@sentry/types": "^8.12.0",
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.0.16",
Expand Down
7 changes: 7 additions & 0 deletions packages/sanity/src/core/error/errorReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export interface ErrorReporter {
* @returns An object containing information on the reported error, or `null` if ignored
*/
reportError: (error: Error, options?: ErrorInfo) => {eventId: string} | null
/**
* In some cases (for example, when we are respecting telemetry consent and not sending data to 3rd parties),
* we may start the error reporter in a pending state, where it will not report errors.
* This method can be used to activate the error reporter.
*/
enable: () => void
disable: () => void
}

/**
Expand Down
63 changes: 63 additions & 0 deletions packages/sanity/src/core/error/sentry/makeBufferedTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {makeFetchTransport} from '@sentry/react'
import {type EventEnvelope, type Transport, type TransportMakeRequestResponse} from '@sentry/types'

/**
* @internal
*/
export type BufferedTransport = Transport & {
setConsent: (consentGiven: boolean) => Promise<void>
}

/**
* Because we want to buffer events until the user has given consent to telemetry,
* we need to implement a custom transport, but mostly wrap the fetch transport.
* @internal
*/
export function makeBufferedTransport(options: any): BufferedTransport {
let buffer: EventEnvelope[] = []
let consentGiven: boolean | undefined
const fetchTransport = makeFetchTransport(options)

const send = async (event: EventEnvelope) => {
if (consentGiven) {
return sendImmediately(event)
}

//we may not have received consent yet. Buffer the event until we know what to do.
if (typeof consentGiven === 'undefined') {
buffer.push(event)
}
// consent not given, skip sending the event
return {}
}

const sendImmediately = async (event: EventEnvelope): Promise<TransportMakeRequestResponse> => {
return fetchTransport.send(event)
}

const setConsent = async (consent: boolean) => {
consentGiven = consent
//we clear the buffer if consent is given (since we've sent the buffered events)
//and we clear the buffer if consent is revoked (since the events should not be sent)
if (consent) {
await flushBuffer()
}
buffer = []
}

const flushBuffer = async () => {
await Promise.all(buffer.map(sendImmediately)).catch((err) => {
console.error('Failed to send buffered events from transport: ', err)
})
}

const flush = async () => {
return fetchTransport.flush()
}

return {
flush,
send,
setConsent,
}
}
27 changes: 24 additions & 3 deletions packages/sanity/src/core/error/sentry/sentryErrorReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ import {
init,
isInitialized as sentryIsInitialized,
linkedErrorsIntegration,
makeFetchTransport,
Scope,
withScope,
} from '@sentry/react'
import {type Transport} from '@sentry/types'

import {isDev} from '../../environment'
import {hasSanityPackageInImportMap} from '../../environment/hasSanityPackageInImportMap'
import {globalScope} from '../../util/globalScope'
import {supportsLocalStorage} from '../../util/supportsLocalStorage'
import {SANITY_VERSION} from '../../version'
import {type ErrorInfo, type ErrorReporter} from '../errorReporter'
import {type BufferedTransport, makeBufferedTransport} from './makeBufferedTransport'

const SANITY_DSN = 'https://8914c8dde7e1ebce191f15af8bf6b7b9@sentry.sanity.io/4507342122123264'

Expand All @@ -44,6 +45,7 @@ const clientOptions: BrowserOptions = {
environment: isDev ? 'development' : 'production',
debug: DEBUG_ERROR_REPORTING,
enabled: IS_BROWSER && (!isDev || DEBUG_ERROR_REPORTING),
transport: makeBufferedTransport,
}

const integrations = [
Expand Down Expand Up @@ -101,10 +103,10 @@ export function getSentryErrorReporter(): ErrorReporter {
if (hasThirdPartySentry) {
client = new BrowserClient({
...clientOptions,
transport: makeFetchTransport,
stackParser: defaultStackParser,
integrations,
beforeSend,
transport: makeBufferedTransport,
})

scope = new Scope()
Expand Down Expand Up @@ -172,9 +174,28 @@ export function getSentryErrorReporter(): ErrorReporter {
return eventId ? {eventId} : null
}

function isBufferedTransport(transport: Transport | undefined): transport is BufferedTransport {
return !!transport && 'setConsent' in transport && typeof transport.setConsent === 'function'
}

function enable() {
const transport = client?.getTransport()
if (isBufferedTransport(transport)) {
transport.setConsent(true)
}
}
function disable() {
const transport = client?.getTransport()
if (isBufferedTransport(transport)) {
transport.setConsent(false)
}
}

return {
initialize,
reportError,
enable,
disable,
}
}

Expand Down Expand Up @@ -317,7 +338,7 @@ function sanityDedupeIntegration() {
}

/**
* Determines wether or not the given event should be dropped or not, based on a window of
* Determines whether or not the given event should be dropped or not, based on a window of
* previously reported events.
*
* @param currentEvent - The event to check
Expand Down
34 changes: 34 additions & 0 deletions packages/sanity/src/core/studio/EnableErrorReporting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {type SanityClient} from '@sanity/client'
import {useEffect} from 'react'

import {errorReporter} from '../error/errorReporter'
import {useClient} from '../hooks'
import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../studioClient'

async function fetchTelemetryConsent(client: SanityClient) {
return client.request({uri: '/intake/telemetry-status'})
}

export function EnableErrorReporting() {
const client = useClient().withConfig(DEFAULT_STUDIO_CLIENT_OPTIONS)

useEffect(() => {
async function checkConsent() {
try {
const res = await fetchTelemetryConsent(client)

if (res?.status === 'granted') {
errorReporter.enable()
} else {
errorReporter.disable()
}
} catch (e) {
console.error('Error fetching telemetry status', e)
}
}

checkConsent()
}, [client])

return null
}
2 changes: 2 additions & 0 deletions packages/sanity/src/core/studio/StudioProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {ActiveWorkspaceMatcher} from './activeWorkspaceMatcher'
import {AuthBoundary} from './AuthBoundary'
import {ColorSchemeProvider} from './colorScheme'
import {Z_OFFSET} from './constants'
import {EnableErrorReporting} from './EnableErrorReporting'
import {PackageVersionStatusProvider} from './packageVersionStatus/PackageVersionStatusProvider'
import {
AuthenticateScreen,
Expand Down Expand Up @@ -66,6 +67,7 @@ export function StudioProvider({
<StudioTelemetryProvider config={config}>
<LocaleProvider>
<PackageVersionStatusProvider>
<EnableErrorReporting />
<ResourceCacheProvider>{children}</ResourceCacheProvider>
</PackageVersionStatusProvider>
</LocaleProvider>
Expand Down
17 changes: 13 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 83e1111

Please sign in to comment.