preface
Sentry is no stranger to front-end engineers. This article explains how sentry is implemented to catch various errors through source code.
- Sentry -javascript repository address
sentry
Basic usage:The official address
Lead to
Let’s start by looking at two key tool approaches
AddInstrumentationHandler secondary packaging native method
We start with the @ the sentry/uitls instrument. The ts file addInstrumentationHandler method:
function addInstrumentationHandler(handler: InstrumentHandler) :void {
if (
!handler ||
typeofhandler.type ! = ='string' ||
typeofhandler.callback ! = ='function'
) {
return;
}
// Initializes the corresponding type of callback
handlers[handler.type] = handlers[handler.type] || [];
// Add callback queue
(handlers[handler.type] as InstrumentHandlerCallback[]).push(handler.callback);
instrument(handler.type);
}
// Global closure
const instrumented: { [key inInstrumentHandlerType]? :boolean } = {};
function instrument(type: InstrumentHandlerType) :void {
if (instrumented[type]) {
return;
}
Global closures prevent double encapsulation
instrumented[type] = true;
switch (type) {
case 'console':
instrumentConsole();
break;
case 'dom':
instrumentDOM();
break;
case 'xhr':
instrumentXHR();
break;
case 'fetch':
instrumentFetch();
break;
case 'history':
instrumentHistory();
break;
case 'error':
instrumentError();
break;
case 'unhandledrejection':
instrumentUnhandledRejection();
break;
default:
logger.warn('unknown instrumentation type:'.type); }}Copy the code
AddInstrumentationHandler collect callback and call the corresponding method of native method for secondary packaging.
Fill wraps a given object method with a higher-order function
function fill(
source: { [key: string] :any }, // Target object
name: string.// Override the field namereplacementFactory: (... args:any[]) = >any // Encapsulate higher-order functions
) :void {
// Non-existent fields are not encapsulated
if(! (namein source)) {
return;
}
// The native method
const original = source[name] as() = >any;
// Higher order function
const wrapped = replacementFactory(original) as WrappedFunction;
if (typeof wrapped === 'function') {
try {
// Specify an empty object prototype for higher-order functions
wrapped.prototype = wrapped.prototype || {};
Object.defineProperties(wrapped, {
__sentry_original__: {
enumerable: false.value: original,
},
});
} catch (_Oo) {
}
}
// override native methods
source[name] = wrapped;
}
Copy the code
Fetch error capture
Specifying URL capture
At sentry initialization, which urls can be captured using tracingOrigins, sentry caches all urls that should be captured using scoped closures, eliminating repeated traversal.
// Scope closure
const urlMap: Record<string.boolean> = {};
// Determine whether the current URL should be captured
const defaultShouldCreateSpan = (url: string) :boolean= > {
if (urlMap[url]) {
return urlMap[url];
}
const origins = tracingOrigins;
// Cache urls without repeated traversal
urlMap[url] =
origins.some((origin: string | RegExp) = >isMatchingPattern(url, origin)) && ! isMatchingPattern(url,'sentry_key');
return urlMap[url];
};
Copy the code
Add a capture callback
Next, we see in @sentry/browser:
if (traceFetch) {
addInstrumentationHandler({
callback: (handlerData: FetchData) = > {
fetchCallback(handlerData, shouldCreateSpan, spans);
},
type: 'fetch'}); }Copy the code
Higher-order functions encapsulate fetch
InstrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch
function instrumentFetch() :void {
if(! supportsNativeFetch()) {return;
}
fill(global.'fetch'.function(originalFetch) {
// The wrapped fetch method
return function(. args:any[]) :void {
const handlerData = {
args,
fetchData: {
method: getFetchMethod(args),
url: getFetchUrl(args),
},
startTimestamp: Date.now(),
};
// Execute the callback methods of fetch Type in sequence
triggerHandlers('fetch', {
...handlerData,
});
// Redirect to this with apply
return originalFetch.apply(global, args).then(
// The request succeeded
(response: Response) = > {
triggerHandlers('fetch', {
...handlerData,
endTimestamp: Date.now(),
response,
});
return response;
},
// The request failed
(error: Error) = > {
triggerHandlers('fetch', {
...handlerData,
endTimestamp: Date.now(),
error,
});
throwerror; }); }; }); }Copy the code
We can use the above code found sentry encapsulates the fetch method, after the end of the request, the priority traversal in addInstrumentationHandler cache callbacks, then continue through to the result to subsequent user callback.
What happens inside the capture callback function
Let’s take a look at what happens in the FETCH callback
export function fetchCallback(
handlerData: FetchData, // Integrate the data content too
shouldCreateSpan: (url: string) = >boolean.// Determine whether the current URL needs to be captured
spans: Record<string, Span>,
) :void {
// Get the user configuration
constcurrentClientOptions = getCurrentHub().getClient()? .getOptions();if(! (currentClientOptions && hasTracingEnabled(currentClientOptions)) || ! (handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url)) ) {return;
}
// Only the request containing the transaction ID is processed
if (handlerData.endTimestamp && handlerData.fetchData.__span) {
const span = spans[handlerData.fetchData.__span];
if (span) {
const response = handlerData.response;
if (response) {
span.setHttpStatus(response.status);
}
span.finish();
delete spans[handlerData.fetchData.__span];
}
return;
}
// Start the request and create a transaction
const activeTransaction = getActiveTransaction();
if (activeTransaction) {
const span = activeTransaction.startChild({
data: {
...handlerData.fetchData,
type: 'fetch',},description: `${handlerData.fetchData.method} ${handlerData.fetchData.url}`.op: 'http'});// Add a unique ID
handlerData.fetchData.__span = span.spanId;
// Record the unique ID
spans[span.spanId] = span;
// Depending on how fetch is used, the first argument can be either the Request address or the Request object
const request = (handlerData.args[0] = handlerData.args[0] as string | Request);
// According to the use of fetch, the second argument is the relevant configuration item of the request
const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string] :any}) | | {});// The configuration item is headers by default (possibly undefined).
let headers = options.headers;
if (isInstanceOf(request, Request)) {
// If request is a request object, headers uses request
headers = (request as Request).headers;
}
if (headers) {
// If the user has set headers, add a sentry-trace field to the request header
if (typeof headers.append === 'function') {
headers.append('sentry-trace', span.toTraceparent());
} else if (Array.isArray(headers)) {
headers = [...headers, ['sentry-trace', span.toTraceparent()]];
} else{ headers = { ... headers,'sentry-trace': span.toTraceparent() }; }}else {
// The user does not set headers
headers = { 'sentry-trace': span.toTraceparent() };
}
// We initialize handlerData.args[1] when we invoke the options declaration, overwriting the fetch header with the reference typeoptions.headers = headers; }}Copy the code
conclusion
At this point, we can know how sentry captures information in the FETCH. Let’s summarize the steps:
- User configuration
traceFetch
Confirm to openfetch
Capture, configuretracingOrigins
Confirm what to captureurl
- through
shouldCreateSpanForRequest
To add tofetch
The callback of the declaration cycle- Internal calls
instrumentFetch
The globalfetch
Do secondary packaging
- Internal calls
- By the user
fetch
Send the request- Integrate reporting information
- Iterate over the callback function you added in the previous step
- Create a unique transaction for reporting information
- in
fetch
In the request headersentry-trace
field
- Call the native method to send the request
- After requesting the response, iterate over the callback function added in the previous step based on the returned status
- When the request succeeds, record the response status
- Report this Request
- End the capture