Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): Add Reporting API server helpers #14044

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>
<head>
<title>Reporting API</title>
</head>
<body>
<!-- <script src="//example.com/script.js"></script> -->
<h1>Reporting API</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAuyGGuR1WxHZI6NzLKBton8MQKwrOgruuH3OX3xgFc6U1ZCec
B59n7zgnNkSHBAv+x+9I1pDnwSFGKtuqSOU1sHC4usRkKNTXN+mU172AdWW7N8mz
np8yFe08OELmRPXgJmo1q/5LXfO/I6MNuKAVHm8MKWn1e9uxlkcPMW1nOXvuPwk8
BcPvXh7w5CSjG7z2wY+D9YkH3KWyAt1oJaETS7iSfdQy+pfZy/Nq6//n/+Nu7yuQ
NIWR73+ejWtQAzi58RHYjlBEOwhtBdxgApJX5OpQMiy+ncFNpAU2YQa/RLRtarNy
mrWqAStr3gBeeL8R61nX57hFoUM6S65cKnKIRQIDAQABAoIBAQCm9H+FkxaBy+K6
15rt2p5aw6ceL9MVsqrkZrZeFclvZzuecvRznJYXSSs68KLhSm5zJRsATGJo3e4D
eN6RkOZ41+kIwQV3pIWr3dutK+Z7V1tUp8F4ySHfjDyJGa7mYdQtkd7257eISFsF
SYmJalHNSFg6bs3VRqpHoHh+qdRJ5K8ZuwXtZg0vBPovJbKcVRDvO59CVXyNpB87
Z4FtP0Z3aTyhCbMjNhs3Zg55qBv/sD7LKJdcGzxHnQIGWqjZMbLC2ncWpcKd9DOv
oPI0J0FeG1wB0EQ+w394SBfK9GSSmFpQA8IR22H61V9n9YQWmv1QRBzQberrx0/B
Fsm19FehAoGBAO7r34JAoj905Lprsa2B1TlkGr+K+Rihda9L+9snzlgErmoAtroa
OFpZpWBeK3TIYEwZfQzavvQjtjWlXo53GaOLbEpJZAoZgx7OLptq9JQvGxP/e56E
6uQP6AyWqYNBE1yQ6nf20oZUkROYugtrNAFhfjjeY3Iv2N/L/bhynkCpAoGBAMiB
6uvcEJ0lRjQxenOcdGyrHogDpyGCpW1AjBM715wzvv8riu+mU0WctPclhAp0ihmI
loW/NVGHHOHrXHTkzxxC8H1TMA7XFSvyBKr/I0E02YTl3aFCYj96d9YUDtJ+ii+A
TEX4LxOEgEaV38FV+NqKYsOUSOFtryV9XN8eaCA9AoGATRq2EPUG52+z+S2UM6h4
xYK21yXkabyBnEbrSri38NPh7be5QKoBkbbolgcOAyw/V4/KOYHGqkd0IC/0Kgnp
rkvqcLbqoA/HrwNfKy0DLWdeV7/VhzziCSPRAW8F2aQAuZjS8lAndM6J20cok1LP
f7qU34l1kn46gU713Lawt7ECgYEAr7Bu+UY15IxxrHAiXMUdms6CDSouOwWwED/7
vSq1bTBGTm6H6h9yXc/HHbvorASbsW3mfsEhZhOe8jJ6LA8Fjzz0XswSkx5RddU1
0+OFr7AwXOvRvGhfkEGuWY3vwu+QA7lGnBSwo0h54d+XVWAQkuWpTrhS9/xU+OOV
Cggpsv0CgYBxLAfSj5ZS50dn+J8FaQypPbCMDKb2gYk7RZ1SpTLBeYmWHYbD/oht
8js8hzj2hfj5Eu2LJWxamjS0dbDlizzVWkbVfk/e/hCPnIurXvUaFwVbkcu20psn
V+wc23onQvNUZUM8cZQ+ApS38NiaX66HvgOVtaOCcCiv2PF7EuOxrA==
-----END RSA PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICljCCAX4CCQChkPJBaaS+ojANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1
czAeFw0yNDEwMjExMjI4MTBaFw0yNTEwMjExMjI4MTBaMA0xCzAJBgNVBAYTAnVz
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuyGGuR1WxHZI6NzLKBto
n8MQKwrOgruuH3OX3xgFc6U1ZCecB59n7zgnNkSHBAv+x+9I1pDnwSFGKtuqSOU1
sHC4usRkKNTXN+mU172AdWW7N8mznp8yFe08OELmRPXgJmo1q/5LXfO/I6MNuKAV
Hm8MKWn1e9uxlkcPMW1nOXvuPwk8BcPvXh7w5CSjG7z2wY+D9YkH3KWyAt1oJaET
S7iSfdQy+pfZy/Nq6//n/+Nu7yuQNIWR73+ejWtQAzi58RHYjlBEOwhtBdxgApJX
5OpQMiy+ncFNpAU2YQa/RLRtarNymrWqAStr3gBeeL8R61nX57hFoUM6S65cKnKI
RQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBnCF6XsEDFWIS4Uknvhj6Ris8iWI4F
JmXVQ3EMBkk7nk9od1OMXcYdSmrJs5eQLiCl+xZOIeA1Gvtrl+NEjWLbKwcgtkTW
0aIkOMxutAVUK+KKqjzkxJ4gvcdKpCtRH+6YllLpYwisdSOk6bby3g0TAfViY2wN
J4iqtjLRq5UWsIPuFPB8y3l4nhavtAHV6uOQNvl6mXEUj9QDE1BsNC8Fs24Eu/jv
DB7da1V8whpm1KDyp1dc7+ioXYTISZ0r2IcK/XRjzfiH1s7Xvf4juYLVYhDSpwdJ
wQgwssHT71WhtvAtn16bike+ZHZbqkiENZRcStDMJiyeHEiSpT/7Kibl
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as https from 'https';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import { defaultStackParser as browserStackParser } from '@sentry/browser';
import { handleReportingApi } from '@sentry/core';
import * as Sentry from '@sentry/node';

const __dirname = new URL('.', import.meta.url).pathname;

Sentry.init({
debug: true,
dsn: 'https://[email protected]/1337',
release: '1.0',
transport: loggingTransport,
});

import { readFileSync } from 'fs';
import { join } from 'path';
import express from 'express';

const app = express();

app.use(express.json({ type: 'application/reports+json' }));

app.post('/reporting-api', async (req, res) => {
await handleReportingApi(req.body, browserStackParser);
res.sendStatus(200);
});

const port = 9000;

app.get('/', (req, res) => {
const file = readFileSync(join(__dirname, 'index.html'), { encoding: 'utf-8' });

res.setHeader('Content-Type', 'text/html');
res.setHeader(
'Reporting-Endpoints',
`csp-endpoint="https://localhost:${port}/reporting-api", default="https://localhost:${port}/reporting-api"`,
);
res.setHeader('Content-Security-Policy', "default-src 'self'; report-to csp-endpoint");
res.setHeader(
'Origin-Trial',
'ApD+E2izWNtaaRBeZ5BXu46aV0l1MSUzJTPERkU3yf+53pAOHj3rARpjb08itVJklPYx7iNEv5//s2dtXUFIvgMAAABzeyJvcmlnaW4iOiJodHRwczovL2xvY2FsaG9zdDo5MDAwIiwiZmVhdHVyZSI6IkRvY3VtZW50UG9saWN5SW5jbHVkZUpTQ2FsbFN0YWNrc0luQ3Jhc2hSZXBvcnRzIiwiZXhwaXJ5IjoxNzQyMzQyMzk5fQ==',
);
res.setHeader('Document-Policy', 'include-js-call-stacks-in-crash-reports');
res.send(file);
});
Fixed Show fixed Hide fixed

const options = {
key: readFileSync(join(__dirname, 'localhost-key.pem')),
cert: readFileSync(join(__dirname, 'localhost.pem')),
};

const server = https.createServer(options, app);

server.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`{"port":${port}}`);
});
49 changes: 49 additions & 0 deletions packages/core/src/envelope.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type {
CSPReport,
CSPReportPayload,
Client,
DeprecatedCSPReport,
DsnComponents,
DynamicSamplingContext,
Event,
EventEnvelope,
EventItem,
RawSecurityEnvelope,
RawSecurityItem,
SdkInfo,
SdkMetadata,
Session,
Expand All @@ -18,8 +23,10 @@ import type {
import {
createEnvelope,
createEventEnvelopeHeaders,
dropUndefinedKeys,
dsnToString,
getSdkMetadataForEnvelopeHeader,
uuid4,
} from '@sentry/utils';
import { createSpanEnvelopeItem } from '@sentry/utils';
import { getDynamicSamplingContextFromSpan } from './tracing/dynamicSamplingContext';
Expand Down Expand Up @@ -135,3 +142,45 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?

return createEnvelope<SpanEnvelope>(headers, items);
}

function convertToDeprecatedPayload(report: CSPReportPayload): DeprecatedCSPReport {
return {
'csp-report': {
'document-uri': report.documentURI,
referrer: report.referrer,
'blocked-uri': report.blockedURI,
'effective-directive': report.effectiveDirective,
'violated-directive': report.effectiveDirective,
'original-policy': report.originalPolicy,
disposition: report.disposition,
'status-code': report.statusCode,
status: report.statusCode.toString(),
'script-sample': report.sourceFile,
sample: report.sample,
},
};
}

/**
* Create an Envelope from a CSP report.
*/
export function createRawSecurityEnvelope(
report: CSPReport,
dsn: DsnComponents,
tunnel?: string,
release?: string,
environment?: string,
): RawSecurityEnvelope {
const envelopeHeaders = {
event_id: uuid4(),
...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
...(report.user_agent && { user_agent: report.user_agent }),
};

const eventItem: RawSecurityItem = [
{ type: 'raw_security', sentry_release: release, sentry_environment: environment },
dropUndefinedKeys(convertToDeprecatedPayload(report.body)),
];

return createEnvelope<RawSecurityEnvelope>(envelopeHeaders, [eventItem]);
}
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,5 @@ export { captureFeedback } from './feedback';
export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim';

export { SDK_VERSION } from '@sentry/utils';

export { handleReportingApi, handlerReportingApiRequest } from './reporting';
83 changes: 83 additions & 0 deletions packages/core/src/reporting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Event, Report, StackParser, Stacktrace } from '@sentry/types';
import { uuid4 } from '@sentry/utils';
import { getClient } from './currentScopes';
import { createRawSecurityEnvelope } from './envelope';

/** Handles Requests from the Reporting API */
export async function handlerReportingApiRequest(
request: Request,
browserStackParser?: StackParser,
client = getClient(),
): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Expected POST', { status: 405 });
}

if (request.headers.get('Content-Type') !== 'application/reports+json') {
return new Response('Expected "application/reports+json" Content-Type', { status: 415 });
}
const reports = await request.json();
await handleReportingApi(reports, browserStackParser, client);
return new Response(undefined, { status: 200 });
}

/** Handles Reports from the Reporting API */
export async function handleReportingApi(
reports: Report[],
browserStackParser?: StackParser,
client = getClient(),
): Promise<void> {
if (!client) {
// eslint-disable-next-line no-console
console.warn('[Reporting API] No client available');
return;
}

const dsn = client.getDsn();
if (!dsn) {
// eslint-disable-next-line no-console
console.warn('[Reporting API] No DSN set');
return;
}

for (const report of reports) {
if (report.type === 'crash') {
const event: Event = {
level: 'fatal',
message: 'Crashed',
request: {
url: report.url,
...(report.user_agent && { headers: { 'User-Agent': report.user_agent } }),
},
};

if (report.body.reason === 'oom') {
event.message = 'Crashed: Out of memory';
} else if (report.body.reason === 'unresponsive') {
event.message = 'Crashed: Unresponsive';
}

if (report.body.stack) {
event.exception = {
values: [
{
type: 'Crashed',
value: event.message,
stacktrace: {
...(browserStackParser && report.body.stack && { frames: browserStackParser(report.body.stack) }),
},
},
],
};
delete event.message;
}

client.captureEvent(event);
} else if (report.type === 'csp-violation') {
const options = client.getOptions();
const envelope = createRawSecurityEnvelope(report, dsn, options.tunnel, options.release, options.environment);

await client.sendEnvelope(envelope);
}
}
}
2 changes: 2 additions & 0 deletions packages/types/src/datacategory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ export type DataCategory =
| 'metric_bucket'
// Span
| 'span'
// Security report
| 'security'
// Unknown data category
| 'unknown';
10 changes: 8 additions & 2 deletions packages/types/src/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Event } from './event';
import type { FeedbackEvent, UserFeedback } from './feedback';
import type { Profile, ProfileChunk } from './profiling';
import type { ReplayEvent, ReplayRecordingData } from './replay';
import type { DeprecatedCSPReport } from './reporting';
import type { SdkInfo } from './sdkinfo';
import type { SerializedSession, SessionAggregates } from './session';
import type { SpanJSON } from './span';
Expand Down Expand Up @@ -41,7 +42,8 @@ export type EnvelopeItemType =
| 'replay_recording'
| 'check_in'
| 'statsd'
| 'span';
| 'span'
| 'raw_security';

export type BaseEnvelopeHeaders = {
[key: string]: unknown;
Expand Down Expand Up @@ -84,6 +86,7 @@ type ProfileItemHeaders = { type: 'profile' };
type ProfileChunkItemHeaders = { type: 'profile_chunk' };
type StatsdItemHeaders = { type: 'statsd'; length: number };
type SpanItemHeaders = { type: 'span' };
type RawSecurityHeaders = { type: 'raw_security'; sentry_release?: string; sentry_environment?: string };

export type EventItem = BaseEnvelopeItem<EventItemHeaders, Event>;
export type AttachmentItem = BaseEnvelopeItem<AttachmentItemHeaders, string | Uint8Array>;
Expand All @@ -100,6 +103,7 @@ export type FeedbackItem = BaseEnvelopeItem<FeedbackItemHeaders, FeedbackEvent>;
export type ProfileItem = BaseEnvelopeItem<ProfileItemHeaders, Profile>;
export type ProfileChunkItem = BaseEnvelopeItem<ProfileChunkItemHeaders, ProfileChunk>;
export type SpanItem = BaseEnvelopeItem<SpanItemHeaders, Partial<SpanJSON>>;
export type RawSecurityItem = BaseEnvelopeItem<RawSecurityHeaders, DeprecatedCSPReport>;

export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext };
type SessionEnvelopeHeaders = { sent_at: string };
Expand All @@ -120,6 +124,7 @@ export type CheckInEnvelope = BaseEnvelope<CheckInEnvelopeHeaders, CheckInItem>;
export type StatsdEnvelope = BaseEnvelope<StatsdEnvelopeHeaders, StatsdItem>;
export type SpanEnvelope = BaseEnvelope<SpanEnvelopeHeaders, SpanItem>;
export type ProfileChunkEnvelope = BaseEnvelope<BaseEnvelopeHeaders, ProfileChunkItem>;
export type RawSecurityEnvelope = BaseEnvelope<BaseEnvelopeHeaders, RawSecurityItem>;

export type Envelope =
| EventEnvelope
Expand All @@ -129,6 +134,7 @@ export type Envelope =
| ReplayEnvelope
| CheckInEnvelope
| StatsdEnvelope
| SpanEnvelope;
| SpanEnvelope
| RawSecurityEnvelope;

export type EnvelopeItem = Envelope[1][number];
3 changes: 3 additions & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export type {
ProfileItem,
ProfileChunkEnvelope,
ProfileChunkItem,
RawSecurityEnvelope,
RawSecurityItem,
SpanEnvelope,
SpanItem,
} from './envelope';
Expand Down Expand Up @@ -173,3 +175,4 @@ export type {
export type { ParameterizedString } from './parameterize';
export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './profiling';
export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy';
export type { Report, CSPReport, CSPReportPayload, DeprecatedCSPReport } from './reporting';
Loading
Loading