Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

## Unreleased

### Features

- Add automatic instrumentation for `CapacitorHttp` requests, including spans, breadcrumbs, and trace propagation using `tracePropagationTargets` and `propagateTraceparent`. ([#1387](https://github.com/getsentry/sentry-capacitor/pull/1387))
-
Comment thread
cursor[bot] marked this conversation as resolved.
### Fixes

- iOS: `enableCaptureFailedRequests` and `sendDefaultPii` are now correctly passed into Sentry Cocoa SDK ([#1384](https://github.com/getsentry/sentry-capacitor/pull/1384))
Expand Down
250 changes: 250 additions & 0 deletions src/integrations/capacitorHttp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import {
Capacitor,
type HttpOptions,
type HttpResponse,
} from '@capacitor/core';
import {
addBreadcrumb,
type Client,
debug,
getBreadcrumbLogLevelFromHttpStatusCode,
getClient,
getTraceData,
hasSpanStreamingEnabled,
type Integration,
setHttpStatus,
shouldPropagateTraceForUrl,
type Span,
startSpan,
stripUrlQueryAndFragment,
} from '@sentry/core';
import { fillTyped } from '../utils/fill';

const INTEGRATION_NAME = 'CapacitorHttp';

type HttpMethod = 'request' | 'get' | 'post' | 'put' | 'patch' | 'delete';

type NativePromise = (
pluginName: string,
methodName: string,
options?: unknown,
) => Promise<unknown>;

type CapacitorWithNativePromise = typeof Capacitor & {
nativePromise: NativePromise;
};

const HTTP_METHODS: HttpMethod[] = [
'request',
'get',
'post',
'put',
'patch',
'delete',
];

function isHttpMethod(method: string): method is HttpMethod {
return HTTP_METHODS.includes(method as HttpMethod);
}

function isHttpOptions(options: unknown): options is HttpOptions {
return (
typeof options === 'object' &&
options !== null &&
'url' in options &&
typeof options.url === 'string'
);
}

export const capacitorHttpIntegration = (): Integration => ({
name: INTEGRATION_NAME,

setupOnce(): void {
if (!Capacitor.isNativePlatform()) {
debug.warn(
`[${INTEGRATION_NAME}] is disabled by not running on a native platform`,
);
return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add a warning so users know why this is disabled:

Suggested change
return;
debug.warn(`[${INTEGRATION_NAME}]` is disabled by not running on a native platform`);
return;

Must import debug from @sentry/core

}

const capacitor = Capacitor as CapacitorWithNativePromise;

if (typeof capacitor.nativePromise !== 'function') {
return;
}

fillTyped(capacitor, 'nativePromise', original => {
return function (
this: CapacitorWithNativePromise,
pluginName: string,
methodName: string,
options?: unknown,
): Promise<unknown> {
if (
pluginName !== INTEGRATION_NAME ||
!isHttpMethod(methodName) ||
!isHttpOptions(options)
) {
return original.call(this, pluginName, methodName, options);
}

const nativeRequest = (
requestOptions: HttpOptions,
): Promise<HttpResponse> =>
original.call(
this,
pluginName,
methodName,
requestOptions,
) as Promise<HttpResponse>;

return instrumentRequest(nativeRequest, this, methodName, options);
};
});
Comment thread
cursor[bot] marked this conversation as resolved.
},
});

function getMethod(method: HttpMethod, options: HttpOptions): string {
return method === 'request'
? (options.method?.toUpperCase() ?? 'GET')
: method.toUpperCase();
}

function addTracingHeaders(
options: HttpOptions,
span: Span,
client: Client,
): HttpOptions {
if (!client) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client already exists here since the previous function that called addTracingHeaders already checked it before invoking it. I would suggest passing the client as a parameter into this function

return options;
}
const { tracePropagationTargets, propagateTraceparent } = client.getOptions();
if (!shouldPropagateTraceForUrl(options.url, tracePropagationTargets)) {
return options;
}

const traceData = getTraceData({
span,
propagateTraceparent,
});

const headers = { ...options.headers };

setHeaderIfMissing(headers, 'sentry-trace', traceData['sentry-trace']);
setHeaderIfMissing(headers, 'traceparent', traceData.traceparent);
mergeBaggageHeader(headers, traceData.baggage);

return {
...options,
headers,
};
}

function findHeaderKey(
headers: Record<string, string>,
name: string,
): string | undefined {
return Object.keys(headers).find(
key => key.toLowerCase() === name.toLowerCase(),
);
}

function setHeaderIfMissing(
headers: Record<string, string>,
name: string,
value: string | undefined,
): void {
if (!value || findHeaderKey(headers, name)) {
return;
}

headers[name] = value;
}

function mergeBaggageHeader(
headers: Record<string, string>,
sentryBaggage: string | undefined,
): void {
if (!sentryBaggage) {
return;
}

const key = findHeaderKey(headers, 'baggage') ?? 'baggage';
const existingValue = headers[key];

// Preserve baggage wich already contains Sentry values
if (existingValue && /(?:^|,)\s*sentry-[^=]*=/.test(existingValue)) {
return;
}

headers[key] = existingValue
? `${existingValue},${sentryBaggage}`
: sentryBaggage;
}
Comment on lines +164 to +183

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this code reducing the if complexity

Suggested change
function mergeBaggageHeader(
headers: Record<string, string>,
sentryBaggage: string | undefined,
): void {
if (!sentryBaggage) {
return;
}
const existingKey = findHeaderKey(headers, 'baggage');
if (!existingKey) {
headers.baggage = sentryBaggage;
return;
}
const existingValue = headers[existingKey];
if (!existingValue) {
headers[existingKey] = sentryBaggage;
return;
}
// Preserve baggage which already contains Sentry Values
if (/(?:^|,)\s*sentry-[^=]*=/.test(existingValue)) {
return;
}
headers[existingKey] = `${existingValue},${sentryBaggage}`;
}
function mergeBaggageHeader(
headers: Record<string, string>,
sentryBaggage: string | undefined,
): void {
if (!sentryBaggage) {
return;
}
const key = findHeaderKey(headers, 'baggage') ?? 'baggage';
const existingValue = headers[key];
// Preserve baggage which already contains Sentry values
if (existingValue && /(?:^|,)\s*sentry-[^=]*=/.test(existingValue)) {
return;
}
headers[key] = existingValue ? `${existingValue},${sentryBaggage}` : sentryBaggage;
}


async function instrumentRequest(
original: (this: unknown, options: HttpOptions) => Promise<HttpResponse>,
thisArg: unknown,
methodName: HttpMethod,
options: HttpOptions,
): Promise<HttpResponse> {
const client = getClient();

if (!client) {
return original.call(thisArg, options);
}

const method = getMethod(methodName, options);
const spanName = `${method} ${stripUrlQueryAndFragment(options.url)}`;

return startSpan(
{
name: spanName,
op: 'http.client',
onlyIfParent: !hasSpanStreamingEnabled(client),
attributes: {
'http.request.method': method,
'url.full': options.url,
'sentry.origin': 'auto.http.capacitor',
},
},
async span => {
// Trace headers, request, status and breadcrumb
const requestOptions = addTracingHeaders(options, span, client);

try {
const response = (await original.call(
thisArg,
requestOptions,
)) as HttpResponse;

setHttpStatus(span, response.status);

addBreadcrumb({
category: 'capacitor.http',
type: 'http',
level: getBreadcrumbLogLevelFromHttpStatusCode(response.status),
data: {
method,
url: options.url,
status_code: response.status,
},
});

return response;
} catch (error) {
addBreadcrumb({
category: 'capacitor.http',
type: 'http',
level: 'error',
data: {
method,
url: options.url,
},
});

throw error;
}
},
);
}
19 changes: 15 additions & 4 deletions src/integrations/default.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { breadcrumbsIntegration, browserApiErrorsIntegration, browserSessionIntegration, globalHandlersIntegration, httpContextIntegration } from '@sentry/browser';
import { dedupeIntegration, eventFiltersIntegration, functionToStringIntegration, type Integration, linkedErrorsIntegration } from '@sentry/core';
import {
breadcrumbsIntegration,
browserApiErrorsIntegration,
browserSessionIntegration,
globalHandlersIntegration,
httpContextIntegration,
} from '@sentry/browser';
import {
dedupeIntegration,
eventFiltersIntegration,
functionToStringIntegration,
type Integration,
linkedErrorsIntegration,
} from '@sentry/core';
import type { CapacitorOptions } from '../options';
import { deviceContextIntegration } from './devicecontext';
import { eventOriginIntegration } from './eventorigin';
Expand All @@ -24,8 +36,7 @@ export function getDefaultIntegrations(
if (options.enableNative) {
integrations.push(deviceContextIntegration());
integrations.push(logEnricherIntegration());
}
else {
} else {
integrations.push(httpContextIntegration());
}

Expand Down
1 change: 1 addition & 0 deletions src/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { nativeReleaseIntegration } from './release';
export { capacitorRewriteFramesIntegration } from './rewriteframes';
export { sdkInfoIntegration } from './sdkinfo';
export { spotlightIntegration } from './spotlight';
export { capacitorHttpIntegration } from './capacitorHttp';
Loading