Commit 6fee37c4 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

feat: trace jsonrpc (#6159)

* feat: maybeTrace

* feat: maybeTrace jsonrpc

* docs: maybeTrace

* test: fix test typing

* fix: pr feedback
parent a2812fcf
import 'components/analytics' import 'components/analytics'
import './jsonrpc'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing' import { BrowserTracing } from '@sentry/tracing'
......
/**
* Traces all JsonRpc requests as spans, reporting them if there is an active transaction.
* This is analagous to import("@sentry/tracing").BrowserTracingOptions.shouldCreateSpanForRequest.
*
* This is able to collect all requests because there is only one copy of @ethersproject/providers, and web3-react wraps
* any external (EIP-1193) provider in that prototype - overriding the prototype will override send for all instances.
*/
import { JsonRpcProvider } from '@ethersproject/providers'
import { maybeTrace } from './trace'
const jsonRpcProviderSend = JsonRpcProvider.prototype.send
JsonRpcProvider.prototype.send = async function LoggingAwareSend(this, method, params) {
maybeTrace('json_rpc', async ({ setTraceData }) => {
setTraceData('method', method)
if (method === 'eth_call') {
// Trim the calldata to the method hash (10 chars) to avoid recording large payloads and sensitive information.
const methodHash = (params[0].data as string).substring(0, 10)
// Override the calldata with the method hash, which is part of the first param.
setTraceData('params', { ...params, [0]: { ...params[0], data: methodHash } })
} else {
setTraceData('params', params)
}
return jsonRpcProviderSend.call(this, method, params)
})
}
import '@sentry/tracing' // required to populate Sentry.startTransaction, which is not included in the core module import '@sentry/tracing' // required to populate Sentry.startTransaction, which is not included in the core module
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import { Hub } from '@sentry/react'
import { Transaction } from '@sentry/tracing' import { Transaction } from '@sentry/tracing'
import assert from 'assert' import assert from 'assert'
import { trace } from './trace' import { maybeTrace, trace } from './trace'
jest.mock('@sentry/react', () => { jest.mock('@sentry/react', () => {
return { return {
getCurrentHub: jest.fn(),
startTransaction: jest.fn(), startTransaction: jest.fn(),
} }
}) })
...@@ -21,16 +23,16 @@ function getTransaction(index = 0): Transaction { ...@@ -21,16 +23,16 @@ function getTransaction(index = 0): Transaction {
return transaction return transaction
} }
describe('trace', () => { beforeEach(() => {
beforeEach(() => {
const Sentry = jest.requireActual('@sentry/react') const Sentry = jest.requireActual('@sentry/react')
startTransaction.mockReset().mockImplementation((context) => { startTransaction.mockReset().mockImplementation((context) => {
const transaction: Transaction = Sentry.startTransaction(context) const transaction: Transaction = Sentry.startTransaction(context)
transaction.initSpanRecorder() transaction.initSpanRecorder()
return transaction return transaction
}) })
}) })
describe('trace', () => {
it('propagates callback', async () => { it('propagates callback', async () => {
await expect(trace('test', () => Promise.resolve('resolved'))).resolves.toBe('resolved') await expect(trace('test', () => Promise.resolve('resolved'))).resolves.toBe('resolved')
await expect(trace('test', () => Promise.reject('rejected'))).rejects.toBe('rejected') await expect(trace('test', () => Promise.reject('rejected'))).rejects.toBe('rejected')
...@@ -142,3 +144,32 @@ describe('trace', () => { ...@@ -142,3 +144,32 @@ describe('trace', () => {
}) })
}) })
}) })
describe('maybeTrace', () => {
const getScope = jest.fn()
beforeEach(() => {
getScope.mockReset()
const hub = { getScope } as unknown as Hub
jest.spyOn(Sentry, 'getCurrentHub').mockReturnValue(hub)
})
it('propagates callback', async () => {
await expect(maybeTrace('test', () => Promise.resolve('resolved'))).resolves.toBe('resolved')
await expect(maybeTrace('test', () => Promise.reject('rejected'))).rejects.toBe('rejected')
})
it('creates a span under the active transaction', async () => {
getScope.mockReturnValue({ getTransaction })
await trace('test', () => {
maybeTrace('child', () => Promise.resolve(), { data: { e: 'e' }, tags: { is_widget: true } })
return Promise.resolve()
})
const transaction = getTransaction()
const span = transaction.spanRecorder?.spans[1]
assert(span)
expect(span.op).toBe('child')
expect(span.data).toEqual({ e: 'e' })
expect(span.tags).toEqual({ is_widget: true })
})
})
...@@ -87,3 +87,17 @@ export async function trace<T>(name: string, callback: TraceCallback<T>, metadat ...@@ -87,3 +87,17 @@ export async function trace<T>(name: string, callback: TraceCallback<T>, metadat
const transaction = Sentry.startTransaction({ name, data: metadata?.data, tags: metadata?.tags }) const transaction = Sentry.startTransaction({ name, data: metadata?.data, tags: metadata?.tags })
return traceSpan(transaction)(callback) return traceSpan(transaction)(callback)
} }
/**
* Traces the callback as part of an already active trace if an active trace exists.
* @param name - The name of your trace.
* @param callback - The callback to trace. The trace will run for the duration of the callback.
* @param metadata - Any data or tags to include in the trace.
*/
export async function maybeTrace<T>(name: string, callback: TraceCallback<T>, metadata?: TraceMetadata): Promise<T> {
const span = Sentry.getCurrentHub()
.getScope()
?.getTransaction()
?.startChild({ op: name, data: metadata?.data, tags: metadata?.tags })
return traceSpan(span)(callback)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment