diff --git a/CHANGELOG.md b/CHANGELOG.md
index a231f2e7e854..e04de0cd43bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 7.80.0
+
+- feat(astro): Add distributed tracing via `` tags (#9483)
+- feat(node): Capture internal server errors in trpc middleware (#9482)
+- feat(remix): Export a type to use for `MetaFunction` parameters (#9493)
+- fix(astro): Mark SDK package as Astro-external (#9509)
+- ref(nextjs): Don't initialize Server SDK during build (#9503)
+
## 7.79.0
- feat(tracing): Add span `origin` to trace context (#9472)
diff --git a/packages/astro/README.md b/packages/astro/README.md
index df68adfa1037..c3321f5b311f 100644
--- a/packages/astro/README.md
+++ b/packages/astro/README.md
@@ -64,8 +64,8 @@ import { sequence } from "astro:middleware";
import * as Sentry from "@sentry/astro";
export const onRequest = sequence(
- Sentry.sentryMiddleware(),
- // Add your other handlers after sentryMiddleware
+ Sentry.handleRequest(),
+ // Add your other handlers after Sentry.handleRequest()
);
```
diff --git a/packages/astro/package.json b/packages/astro/package.json
index d440be51bb9e..531b99bc372e 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -74,5 +74,8 @@
},
"volta": {
"extends": "../../package.json"
+ },
+ "astro": {
+ "external": true
}
}
diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts
new file mode 100644
index 000000000000..7257cf01db51
--- /dev/null
+++ b/packages/astro/src/server/meta.ts
@@ -0,0 +1,78 @@
+import { getDynamicSamplingContextFromClient } from '@sentry/core';
+import type { Hub, Span } from '@sentry/types';
+import {
+ dynamicSamplingContextToSentryBaggageHeader,
+ generateSentryTraceHeader,
+ logger,
+ TRACEPARENT_REGEXP,
+} from '@sentry/utils';
+
+/**
+ * Extracts the tracing data from the current span or from the client's scope
+ * (via transaction or propagation context) and renders the data to tags.
+ *
+ * This function creates two serialized tags:
+ * - ``
+ * - ``
+ *
+ * TODO: Extract this later on and export it from the Core or Node SDK
+ *
+ * @param span the currently active span
+ * @param client the SDK's client
+ *
+ * @returns an object with the two serialized tags
+ */
+export function getTracingMetaTags(span: Span | undefined, hub: Hub): { sentryTrace: string; baggage?: string } {
+ const scope = hub.getScope();
+ const client = hub.getClient();
+ const { dsc, sampled, traceId } = scope.getPropagationContext();
+ const transaction = span?.transaction;
+
+ const sentryTrace = span ? span.toTraceparent() : generateSentryTraceHeader(traceId, undefined, sampled);
+
+ const dynamicSamplingContext = transaction
+ ? transaction.getDynamicSamplingContext()
+ : dsc
+ ? dsc
+ : client
+ ? getDynamicSamplingContextFromClient(traceId, client, scope)
+ : undefined;
+
+ const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
+
+ const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace);
+ if (!isValidSentryTraceHeader) {
+ logger.warn('Invalid sentry-trace data. Returning empty tag');
+ }
+
+ const validBaggage = isValidBaggageString(baggage);
+ if (!validBaggage) {
+ logger.warn('Invalid baggage data. Returning empty tag');
+ }
+
+ return {
+ sentryTrace: ``,
+ baggage: baggage && ``,
+ };
+}
+
+/**
+ * Tests string against baggage spec as defined in:
+ *
+ * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition
+ * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
+ *
+ * exported for testing
+ */
+export function isValidBaggageString(baggage?: string): boolean {
+ if (!baggage || !baggage.length) {
+ return false;
+ }
+ const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+";
+ const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+';
+ const spaces = '\\s*';
+ const baggageRegex = new RegExp(
+ `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`,
+ );
+ return baggageRegex.test(baggage);
+}
diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts
index ff4ac1d44c78..c04618cad33f 100644
--- a/packages/astro/src/server/middleware.ts
+++ b/packages/astro/src/server/middleware.ts
@@ -1,13 +1,19 @@
-import { captureException, configureScope, startSpan } from '@sentry/node';
+import { captureException, configureScope, getCurrentHub, startSpan } from '@sentry/node';
+import type { Hub, Span } from '@sentry/types';
import { addExceptionMechanism, objectify, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils';
import type { APIContext, MiddlewareResponseHandler } from 'astro';
+import { getTracingMetaTags } from './meta';
+
type MiddlewareOptions = {
/**
* If true, the client IP will be attached to the event by calling `setUser`.
- * Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
*
- * This will only work if your app is configured for SSR
+ * Important: Only enable this option if your Astro app is configured for (hybrid) SSR
+ * via the `output: 'server' | 'hybrid'` option in your `astro.config.mjs` file.
+ * Otherwise, Astro will throw an error when starting the server.
+ *
+ * Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
*
* @default false (recommended)
*/
@@ -15,6 +21,7 @@ type MiddlewareOptions = {
/**
* If true, the headers from the request will be attached to the event by calling `setExtra`.
+ *
* Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
*
* @default false (recommended)
@@ -93,11 +100,42 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
},
},
async span => {
- const res = await next();
- if (span && res.status) {
- span.setHttpStatus(res.status);
+ const originalResponse = await next();
+
+ if (span && originalResponse.status) {
+ span.setHttpStatus(originalResponse.status);
+ }
+
+ const hub = getCurrentHub();
+ const client = hub.getClient();
+ const contentType = originalResponse.headers.get('content-type');
+
+ const isPageloadRequest = contentType && contentType.startsWith('text/html');
+ if (!isPageloadRequest || !client) {
+ return originalResponse;
}
- return res;
+
+ // Type case necessary b/c the body's ReadableStream type doesn't include
+ // the async iterator that is actually available in Node
+ // We later on use the async iterator to read the body chunks
+ // see https://github.com/microsoft/TypeScript/issues/39051
+ const originalBody = originalResponse.body as NodeJS.ReadableStream | null;
+ if (!originalBody) {
+ return originalResponse;
+ }
+
+ const newResponseStream = new ReadableStream({
+ start: async controller => {
+ for await (const chunk of originalBody) {
+ const html = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
+ const modifiedHtml = addMetaTagToHead(html, hub, span);
+ controller.enqueue(new TextEncoder().encode(modifiedHtml));
+ }
+ controller.close();
+ },
+ });
+
+ return new Response(newResponseStream, originalResponse);
},
);
return res;
@@ -109,6 +147,20 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
};
};
+/**
+ * This function optimistically assumes that the HTML coming in chunks will not be split
+ * within the
tag. If this still happens, we simply won't replace anything.
+ */
+function addMetaTagToHead(htmlChunk: string, hub: Hub, span?: Span): string {
+ if (typeof htmlChunk !== 'string') {
+ return htmlChunk;
+ }
+
+ const { sentryTrace, baggage } = getTracingMetaTags(span, hub);
+ const content = `\n${sentryTrace}\n${baggage}\n`;
+ return htmlChunk.replace('', content);
+}
+
/**
* Interpolates the route from the URL and the passed params.
* Best we can do to get a route name instead of a raw URL.
diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts
new file mode 100644
index 000000000000..6298f5f2a20b
--- /dev/null
+++ b/packages/astro/test/server/meta.test.ts
@@ -0,0 +1,178 @@
+import * as SentryCore from '@sentry/core';
+import { vi } from 'vitest';
+
+import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta';
+
+const mockedSpan = {
+ toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
+ transaction: {
+ getDynamicSamplingContext: () => ({
+ environment: 'production',
+ }),
+ },
+};
+
+const mockedHub = {
+ getScope: () => ({
+ getPropagationContext: () => ({
+ traceId: '123',
+ }),
+ }),
+ getClient: () => ({}),
+};
+
+describe('getTracingMetaTags', () => {
+ it('returns the tracing tags from the span, if it is provided', () => {
+ {
+ // @ts-expect-error - only passing a partial span object
+ const tags = getTracingMetaTags(mockedSpan, mockedHub);
+
+ expect(tags).toEqual({
+ sentryTrace: '',
+ baggage: '',
+ });
+ }
+ });
+
+ it('returns propagationContext DSC data if no span is available', () => {
+ const tags = getTracingMetaTags(undefined, {
+ ...mockedHub,
+ // @ts-expect-error - only passing a partial scope object
+ getScope: () => ({
+ getPropagationContext: () => ({
+ traceId: '12345678901234567890123456789012',
+ sampled: true,
+ spanId: '1234567890123456',
+ dsc: {
+ environment: 'staging',
+ public_key: 'key',
+ trace_id: '12345678901234567890123456789012',
+ },
+ }),
+ }),
+ });
+
+ expect(tags).toEqual({
+ sentryTrace: expect.stringMatching(
+ //,
+ ),
+ baggage:
+ '',
+ });
+ });
+
+ it('returns only the `sentry-trace` tag if no DSC is available', () => {
+ vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
+ trace_id: '',
+ public_key: undefined,
+ });
+
+ const tags = getTracingMetaTags(
+ // @ts-expect-error - only passing a partial span object
+ {
+ toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
+ transaction: undefined,
+ },
+ mockedHub,
+ );
+
+ expect(tags).toEqual({
+ sentryTrace: '',
+ });
+ });
+
+ it('returns only the `sentry-trace` tag if no DSC is available', () => {
+ vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
+ trace_id: '',
+ public_key: undefined,
+ });
+
+ const tags = getTracingMetaTags(
+ // @ts-expect-error - only passing a partial span object
+ {
+ toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
+ transaction: undefined,
+ },
+ {
+ ...mockedHub,
+ getClient: () => undefined,
+ },
+ );
+
+ expect(tags).toEqual({
+ sentryTrace: '',
+ });
+ });
+});
+
+describe('isValidBaggageString', () => {
+ it.each([
+ 'sentry-environment=production',
+ 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=abc',
+ // @ is allowed in values
+ 'sentry-release=project@1.0.0',
+ // spaces are allowed around the delimiters
+ 'sentry-environment=staging , sentry-public_key=key ,sentry-release=myproject@1.0.0',
+ 'sentry-environment=staging , thirdparty=value ,sentry-release=myproject@1.0.0',
+ // these characters are explicitly allowed for keys in the baggage spec:
+ "!#$%&'*+-.^_`|~1234567890abcxyzABCXYZ=true",
+ // special characters in values are fine (except for ",;\ - see other test)
+ 'key=(value)',
+ 'key=[{(value)}]',
+ 'key=some$value',
+ 'key=more#value',
+ 'key=max&value',
+ 'key=max:value',
+ 'key=x=value',
+ ])('returns true if the baggage string is valid (%s)', baggageString => {
+ expect(isValidBaggageString(baggageString)).toBe(true);
+ });
+
+ it.each([
+ // baggage spec doesn't permit leading spaces
+ ' sentry-environment=production,sentry-publickey=key,sentry-trace_id=abc',
+ // no spaces in keys or values
+ 'sentry-public key=key',
+ 'sentry-publickey=my key',
+ // no delimiters ("(),/:;<=>?@[\]{}") in keys
+ 'asdf(x=value',
+ 'asdf)x=value',
+ 'asdf,x=value',
+ 'asdf/x=value',
+ 'asdf:x=value',
+ 'asdf;x=value',
+ 'asdfx=value',
+ 'asdf?x=value',
+ 'asdf@x=value',
+ 'asdf[x=value',
+ 'asdf]x=value',
+ 'asdf\\x=value',
+ 'asdf{x=value',
+ 'asdf}x=value',
+ // no ,;\" in values
+ 'key=va,lue',
+ 'key=va;lue',
+ 'key=va\\lue',
+ 'key=va"lue"',
+ // baggage headers can have properties but we currently don't support them
+ 'sentry-environment=production;prop1=foo;prop2=bar,nextkey=value',
+ // no fishy stuff
+ 'absolutely not a valid baggage string',
+ 'val"/>',
+ 'something"/>',
+ '',
+ '/>',
+ '" onblur="alert("xss")',
+ ])('returns false if the baggage string is invalid (%s)', baggageString => {
+ expect(isValidBaggageString(baggageString)).toBe(false);
+ });
+
+ it('returns false if the baggage string is empty', () => {
+ expect(isValidBaggageString('')).toBe(false);
+ });
+
+ it('returns false if the baggage string is empty', () => {
+ expect(isValidBaggageString(undefined)).toBe(false);
+ });
+});
diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts
index af42348a94b9..058982f06cc6 100644
--- a/packages/astro/test/server/middleware.test.ts
+++ b/packages/astro/test/server/middleware.test.ts
@@ -4,8 +4,16 @@ import { vi } from 'vitest';
import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware';
+vi.mock('../../src/server/meta', () => ({
+ getTracingMetaTags: () => ({
+ sentryTrace: '',
+ baggage: '',
+ }),
+}));
+
describe('sentryMiddleware', () => {
const startSpanSpy = vi.spyOn(SentryNode, 'startSpan');
+ const nextResult = Promise.resolve(new Response(null, { status: 200, headers: new Headers() }));
afterEach(() => {
vi.clearAllMocks();
@@ -24,7 +32,6 @@ describe('sentryMiddleware', () => {
id: '123',
},
};
- const nextResult = Promise.resolve({ status: 200 });
const next = vi.fn(() => nextResult);
// @ts-expect-error, a partial ctx object is fine here
@@ -109,7 +116,7 @@ describe('sentryMiddleware', () => {
params: {},
url: new URL('https://myDomain.io/users/'),
};
- const next = vi.fn();
+ const next = vi.fn(() => nextResult);
// @ts-expect-error, a partial ctx object is fine here
await middleware(ctx, next);
@@ -159,7 +166,7 @@ describe('sentryMiddleware', () => {
params: {},
url: new URL('https://myDomain.io/users/'),
};
- const next = vi.fn();
+ const next = vi.fn(() => nextResult);
// @ts-expect-error, a partial ctx object is fine here
await middleware(ctx, next);
@@ -178,6 +185,95 @@ describe('sentryMiddleware', () => {
expect.any(Function), // the `next` function
);
});
+
+ it('injects tracing tags into the HTML of a pageload response', async () => {
+ vi.spyOn(SentryNode, 'getCurrentHub').mockImplementation(() => ({
+ // @ts-expect-error this is fine
+ getClient: () => ({}),
+ }));
+
+ const middleware = handleRequest();
+
+ const ctx = {
+ request: {
+ method: 'GET',
+ url: '/users',
+ headers: new Headers(),
+ },
+ params: {},
+ url: new URL('https://myDomain.io/users/'),
+ };
+ const next = vi.fn(() =>
+ Promise.resolve(
+ new Response('', {
+ headers: new Headers({ 'content-type': 'text/html' }),
+ }),
+ ),
+ );
+
+ // @ts-expect-error, a partial ctx object is fine here
+ const resultFromNext = await middleware(ctx, next);
+
+ expect(resultFromNext?.headers.get('content-type')).toEqual('text/html');
+
+ const html = await resultFromNext?.text();
+
+ expect(html).toContain(' {
+ const middleware = handleRequest();
+
+ const ctx = {
+ request: {
+ method: 'GET',
+ url: '/users',
+ headers: new Headers(),
+ },
+ params: {},
+ url: new URL('https://myDomain.io/users/'),
+ };
+
+ const originalResponse = new Response('{"foo": "bar"}', {
+ headers: new Headers({ 'content-type': 'application/json' }),
+ });
+ const next = vi.fn(() => Promise.resolve(originalResponse));
+
+ // @ts-expect-error, a partial ctx object is fine here
+ const resultFromNext = await middleware(ctx, next);
+
+ expect(resultFromNext).toBe(originalResponse);
+ });
+
+ it("no-ops if there's no tag in the response", async () => {
+ const middleware = handleRequest();
+
+ const ctx = {
+ request: {
+ method: 'GET',
+ url: '/users',
+ headers: new Headers(),
+ },
+ params: {},
+ url: new URL('https://myDomain.io/users/'),
+ };
+
+ const originalHtml = 'no head
';
+ const originalResponse = new Response(originalHtml, {
+ headers: new Headers({ 'content-type': 'text/html' }),
+ });
+ const next = vi.fn(() => Promise.resolve(originalResponse));
+
+ // @ts-expect-error, a partial ctx object is fine here
+ const resultFromNext = await middleware(ctx, next);
+
+ expect(resultFromNext?.headers.get('content-type')).toEqual('text/html');
+
+ const html = await resultFromNext?.text();
+
+ expect(html).toBe(originalHtml);
+ });
});
describe('interpolateRouteFromUrlAndParams', () => {
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index a912475e6c41..d2b856a7cb37 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -4,6 +4,7 @@ export type { OfflineStore, OfflineTransportOptions } from './transports/offline
export type { ServerRuntimeClientOptions } from './server-runtime-client';
export * from './tracing';
+export { createEventEnvelope } from './envelope';
export {
addBreadcrumb,
captureCheckIn,
diff --git a/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts b/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts
index 734bda58c10f..7c84f56d178f 100644
--- a/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts
@@ -20,7 +20,7 @@ const port = 3030;
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/create-next-app/tests/behaviour-client.test.ts b/packages/e2e-tests/test-applications/create-next-app/tests/behaviour-client.test.ts
index d0ede66cf32f..54d52d32172b 100644
--- a/packages/e2e-tests/test-applications/create-next-app/tests/behaviour-client.test.ts
+++ b/packages/e2e-tests/test-applications/create-next-app/tests/behaviour-client.test.ts
@@ -4,7 +4,7 @@ import axios, { AxiosError } from 'axios';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Sends a client-side exception to Sentry', async ({ page }) => {
await page.goto('/');
diff --git a/packages/e2e-tests/test-applications/create-next-app/tests/behaviour-server.test.ts b/packages/e2e-tests/test-applications/create-next-app/tests/behaviour-server.test.ts
index 3b3d74af1885..a864701afa9b 100644
--- a/packages/e2e-tests/test-applications/create-next-app/tests/behaviour-server.test.ts
+++ b/packages/e2e-tests/test-applications/create-next-app/tests/behaviour-server.test.ts
@@ -4,7 +4,7 @@ import axios, { AxiosError } from 'axios';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Sends a server-side exception to Sentry', async ({ baseURL }) => {
const { data } = await axios.get(`${baseURL}/api/error`);
diff --git a/packages/e2e-tests/test-applications/create-remix-app-v2/app/root.tsx b/packages/e2e-tests/test-applications/create-remix-app-v2/app/root.tsx
index d4b2c07516eb..cf8d185866a9 100644
--- a/packages/e2e-tests/test-applications/create-remix-app-v2/app/root.tsx
+++ b/packages/e2e-tests/test-applications/create-remix-app-v2/app/root.tsx
@@ -1,5 +1,5 @@
import { cssBundleHref } from '@remix-run/css-bundle';
-import { json, LinksFunction } from '@remix-run/node';
+import { json, LinksFunction, MetaFunction } from '@remix-run/node';
import {
Links,
LiveReload,
@@ -11,6 +11,7 @@ import {
useRouteError,
} from '@remix-run/react';
import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix';
+import type { SentryMetaArgs } from '@sentry/remix';
export const links: LinksFunction = () => [...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : [])];
@@ -22,6 +23,22 @@ export const loader = () => {
});
};
+export const meta = ({ data }: SentryMetaArgs>) => {
+ return [
+ {
+ env: data.ENV,
+ },
+ {
+ name: 'sentry-trace',
+ content: data.sentryTrace,
+ },
+ {
+ name: 'baggage',
+ content: data.sentryBaggage,
+ },
+ ];
+};
+
export function ErrorBoundary() {
const error = useRouteError();
const eventId = captureRemixErrorBoundaryError(error);
diff --git a/packages/e2e-tests/test-applications/create-remix-app-v2/playwright.config.ts b/packages/e2e-tests/test-applications/create-remix-app-v2/playwright.config.ts
index 33f00235771b..79efcbc22c1a 100644
--- a/packages/e2e-tests/test-applications/create-remix-app-v2/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/create-remix-app-v2/playwright.config.ts
@@ -9,7 +9,7 @@ const port = 3030;
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-client.test.ts b/packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-client.test.ts
index e07481075f5a..3b39c9a4ca0b 100644
--- a/packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-client.test.ts
+++ b/packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-client.test.ts
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test';
import axios, { AxiosError } from 'axios';
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
@@ -206,3 +206,31 @@ test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) =
)
.toBe(200);
});
+
+test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => {
+ await page.goto('/');
+
+ const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
+ state: 'attached',
+ });
+ const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
+ state: 'attached',
+ });
+
+ expect(sentryTraceMetaTag).toBeTruthy();
+ expect(baggageMetaTag).toBeTruthy();
+});
+
+test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => {
+ await page.goto('/user/123');
+
+ const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
+ state: 'attached',
+ });
+ const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
+ state: 'attached',
+ });
+
+ expect(sentryTraceMetaTag).toBeTruthy();
+ expect(baggageMetaTag).toBeTruthy();
+});
diff --git a/packages/e2e-tests/test-applications/create-remix-app/app/root.tsx b/packages/e2e-tests/test-applications/create-remix-app/app/root.tsx
index d4b2c07516eb..a015aba78f6c 100644
--- a/packages/e2e-tests/test-applications/create-remix-app/app/root.tsx
+++ b/packages/e2e-tests/test-applications/create-remix-app/app/root.tsx
@@ -1,5 +1,5 @@
import { cssBundleHref } from '@remix-run/css-bundle';
-import { json, LinksFunction } from '@remix-run/node';
+import { json, LinksFunction, MetaFunction } from '@remix-run/node';
import {
Links,
LiveReload,
@@ -22,6 +22,19 @@ export const loader = () => {
});
};
+export const meta: MetaFunction = ({ data }) => {
+ return [
+ {
+ name: 'sentry-trace',
+ content: data.sentryTrace,
+ },
+ {
+ name: 'baggage',
+ content: data.sentryBaggage,
+ },
+ ];
+};
+
export function ErrorBoundary() {
const error = useRouteError();
const eventId = captureRemixErrorBoundaryError(error);
diff --git a/packages/e2e-tests/test-applications/create-remix-app/playwright.config.ts b/packages/e2e-tests/test-applications/create-remix-app/playwright.config.ts
index b35659cecdb5..785ca43321a4 100644
--- a/packages/e2e-tests/test-applications/create-remix-app/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/create-remix-app/playwright.config.ts
@@ -9,7 +9,7 @@ const port = 3030;
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-client.test.ts b/packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-client.test.ts
index e07481075f5a..3b39c9a4ca0b 100644
--- a/packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-client.test.ts
+++ b/packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-client.test.ts
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test';
import axios, { AxiosError } from 'axios';
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
@@ -206,3 +206,31 @@ test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) =
)
.toBe(200);
});
+
+test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => {
+ await page.goto('/');
+
+ const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
+ state: 'attached',
+ });
+ const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
+ state: 'attached',
+ });
+
+ expect(sentryTraceMetaTag).toBeTruthy();
+ expect(baggageMetaTag).toBeTruthy();
+});
+
+test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => {
+ await page.goto('/user/123');
+
+ const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
+ state: 'attached',
+ });
+ const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
+ state: 'attached',
+ });
+
+ expect(sentryTraceMetaTag).toBeTruthy();
+ expect(baggageMetaTag).toBeTruthy();
+});
diff --git a/packages/e2e-tests/test-applications/debug-id-sourcemaps/tests/server.test.ts b/packages/e2e-tests/test-applications/debug-id-sourcemaps/tests/server.test.ts
index df41adb615c8..51f272fa514a 100644
--- a/packages/e2e-tests/test-applications/debug-id-sourcemaps/tests/server.test.ts
+++ b/packages/e2e-tests/test-applications/debug-id-sourcemaps/tests/server.test.ts
@@ -5,7 +5,7 @@ import path from 'path';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test(
'Find symbolicated event on sentry',
diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts
index 391b5eac610e..ab3c40a21471 100644
--- a/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts
@@ -21,7 +21,7 @@ const eventProxyPort = 3031;
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts
index d31e869fd8b3..5383a43c34a9 100644
--- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts
+++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts
@@ -5,7 +5,7 @@ import axios, { AxiosError } from 'axios';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Sends a client-side exception to Sentry', async ({ page }) => {
await page.goto('/');
diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
index 8413d623eb5c..b57538e3b90c 100644
--- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
+++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
@@ -5,7 +5,7 @@ import axios, { AxiosError } from 'axios';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Sends a pageload transaction', async ({ page }) => {
const pageloadTransactionEventPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => {
diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts
index f39997dc76e8..8565f228f271 100644
--- a/packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts
@@ -10,7 +10,7 @@ const eventProxyPort = 3031;
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts
index 4656ba23e7de..70a0a9c660e0 100644
--- a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts
+++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts
@@ -4,7 +4,7 @@ import axios, { AxiosError } from 'axios';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Sends exception to Sentry', async ({ baseURL }) => {
const { data } = await axios.get(`${baseURL}/test-error`);
diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts
index d069fd299682..b05228b21311 100644
--- a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts
+++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts
@@ -6,7 +6,7 @@ import { waitForTransaction } from '../event-proxy-server';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => {
diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts
index b9241d5a2569..b89297c1689d 100644
--- a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts
+++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts
@@ -5,7 +5,7 @@ import axios, { AxiosError } from 'axios';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Sends an API route transaction', async ({ baseURL }) => {
const pageloadTransactionEventPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => {
diff --git a/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts b/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts
index d3fbb6971415..58192fc98bb6 100644
--- a/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts
@@ -15,7 +15,7 @@ const expressPort = 3030;
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts b/packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts
index 4429c2d46edd..2443fcf55f66 100644
--- a/packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts
+++ b/packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts
@@ -5,7 +5,7 @@ import { waitForError } from '../event-proxy-server';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Sends exception to Sentry', async ({ baseURL }) => {
const { data } = await axios.get(`${baseURL}/test-error`);
diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/playwright.config.ts b/packages/e2e-tests/test-applications/react-create-hash-router/playwright.config.ts
index c68482378d7a..5f93f826ebf0 100644
--- a/packages/e2e-tests/test-applications/react-create-hash-router/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/react-create-hash-router/playwright.config.ts
@@ -7,7 +7,7 @@ import { devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/tests/behaviour-test.spec.ts b/packages/e2e-tests/test-applications/react-create-hash-router/tests/behaviour-test.spec.ts
index 43d8eeec8f35..5397dfa8eb64 100644
--- a/packages/e2e-tests/test-applications/react-create-hash-router/tests/behaviour-test.spec.ts
+++ b/packages/e2e-tests/test-applications/react-create-hash-router/tests/behaviour-test.spec.ts
@@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test';
import axios, { AxiosError } from 'axios';
import { ReplayRecordingData } from './fixtures/ReplayRecordingData';
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/playwright.config.ts b/packages/e2e-tests/test-applications/react-router-6-use-routes/playwright.config.ts
index c68482378d7a..5f93f826ebf0 100644
--- a/packages/e2e-tests/test-applications/react-router-6-use-routes/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/playwright.config.ts
@@ -7,7 +7,7 @@ import { devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/behaviour-test.spec.ts b/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/behaviour-test.spec.ts
index 2bda1adcbf9c..4800c9e7cb0e 100644
--- a/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/behaviour-test.spec.ts
+++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/behaviour-test.spec.ts
@@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test';
import axios, { AxiosError } from 'axios';
import { ReplayRecordingData } from './fixtures/ReplayRecordingData';
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts
index c68482378d7a..5f93f826ebf0 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts
@@ -7,7 +7,7 @@ import { devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tests/behaviour-test.spec.ts b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tests/behaviour-test.spec.ts
index 8ff0e686feb4..632d2bfe9d55 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tests/behaviour-test.spec.ts
+++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/tests/behaviour-test.spec.ts
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test';
import axios, { AxiosError } from 'axios';
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/playwright.config.ts b/packages/e2e-tests/test-applications/standard-frontend-react/playwright.config.ts
index c68482378d7a..5f93f826ebf0 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/playwright.config.ts
@@ -7,7 +7,7 @@ import { devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts b/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
index 2bda1adcbf9c..4800c9e7cb0e 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
@@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test';
import axios, { AxiosError } from 'axios';
import { ReplayRecordingData } from './fixtures/ReplayRecordingData';
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
diff --git a/packages/e2e-tests/test-applications/sveltekit/playwright.config.ts b/packages/e2e-tests/test-applications/sveltekit/playwright.config.ts
index e38177e51a90..bfa29df7d549 100644
--- a/packages/e2e-tests/test-applications/sveltekit/playwright.config.ts
+++ b/packages/e2e-tests/test-applications/sveltekit/playwright.config.ts
@@ -15,7 +15,7 @@ const port = 3030;
const config: PlaywrightTestConfig = {
testDir: './test',
/* Maximum time one test can run for. */
- timeout: 60 * 1000,
+ timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
diff --git a/packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts b/packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts
index edc36cb5ca9c..2a069a79b22f 100644
--- a/packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts
+++ b/packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts
@@ -6,7 +6,7 @@ import axios, { AxiosError } from 'axios';
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
-const EVENT_POLLING_TIMEOUT = 30_000;
+const EVENT_POLLING_TIMEOUT = 90_000;
test('Sends a pageload transaction', async ({ page }) => {
const pageloadTransactionEventPromise = waitForTransaction('sveltekit', (transactionEvent: any) => {
diff --git a/packages/feedback/src/index.ts b/packages/feedback/src/index.ts
index 31efcfca2386..57b6075dfca5 100644
--- a/packages/feedback/src/index.ts
+++ b/packages/feedback/src/index.ts
@@ -1,2 +1,2 @@
-export { sendFeedbackRequest } from './util/sendFeedbackRequest';
+export { sendFeedback } from './sendFeedback';
export { Feedback } from './integration';
diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts
index e73868169b16..6269e8a697a8 100644
--- a/packages/feedback/src/types/index.ts
+++ b/packages/feedback/src/types/index.ts
@@ -1,26 +1,10 @@
-import type { Event, Primitive } from '@sentry/types';
+import type { Primitive } from '@sentry/types';
import type { ActorComponent } from '../widget/Actor';
import type { DialogComponent } from '../widget/Dialog';
export type SentryTags = { [key: string]: Primitive } | undefined;
-/**
- * NOTE: These types are still considered Beta and subject to change.
- * @hidden
- */
-export interface FeedbackEvent extends Event {
- feedback: {
- message: string;
- url: string;
- contact_email?: string;
- name?: string;
- replay_id?: string;
- };
- // TODO: Add this event type to Event
- // type: 'feedback_event';
-}
-
export interface SendFeedbackData {
feedback: {
message: string;
diff --git a/packages/feedback/src/util/handleFeedbackSubmit.ts b/packages/feedback/src/util/handleFeedbackSubmit.ts
index 8388300a5a4c..6e7b7c014de4 100644
--- a/packages/feedback/src/util/handleFeedbackSubmit.ts
+++ b/packages/feedback/src/util/handleFeedbackSubmit.ts
@@ -1,18 +1,22 @@
+import type { TransportMakeRequestResponse } from '@sentry/types';
+import { logger } from '@sentry/utils';
+
import { sendFeedback } from '../sendFeedback';
import type { FeedbackFormData, SendFeedbackOptions } from '../types';
import type { DialogComponent } from '../widget/Dialog';
/**
- * Calls `sendFeedback` to send feedback, handles UI behavior of dialog.
+ * Handles UI behavior of dialog when feedback is submitted, calls
+ * `sendFeedback` to send feedback.
*/
export async function handleFeedbackSubmit(
dialog: DialogComponent | null,
feedback: FeedbackFormData,
options?: SendFeedbackOptions,
-): Promise {
+): Promise {
if (!dialog) {
// Not sure when this would happen
- return false;
+ return;
}
const showFetchError = (): void => {
@@ -22,21 +26,15 @@ export async function handleFeedbackSubmit(
dialog.showError('There was a problem submitting feedback, please wait and try again.');
};
+ dialog.hideError();
+
try {
- dialog.hideError();
const resp = await sendFeedback(feedback, options);
- if (!resp) {
- // Errored... re-enable submit button
- showFetchError();
- return false;
- }
-
// Success!
return resp;
- } catch {
- // Errored... re-enable submit button
+ } catch (err) {
+ __DEBUG_BUILD__ && logger.error(err);
showFetchError();
- return false;
}
}
diff --git a/packages/feedback/src/util/prepareFeedbackEvent.ts b/packages/feedback/src/util/prepareFeedbackEvent.ts
index 6d32506d3be4..9b1b0f9c6e8b 100644
--- a/packages/feedback/src/util/prepareFeedbackEvent.ts
+++ b/packages/feedback/src/util/prepareFeedbackEvent.ts
@@ -1,8 +1,6 @@
import type { Scope } from '@sentry/core';
import { prepareEvent } from '@sentry/core';
-import type { Client } from '@sentry/types';
-
-import type { FeedbackEvent } from '../types';
+import type { Client, FeedbackEvent } from '@sentry/types';
interface PrepareFeedbackEventParams {
client: Client;
@@ -17,7 +15,7 @@ export async function prepareFeedbackEvent({
scope,
event,
}: PrepareFeedbackEventParams): Promise {
- const eventHint = { integrations: undefined };
+ const eventHint = {};
if (client.emit) {
client.emit('preprocessEvent', event, eventHint);
}
@@ -25,12 +23,14 @@ export async function prepareFeedbackEvent({
const preparedEvent = (await prepareEvent(
client.getOptions(),
event,
- { integrations: undefined },
+ eventHint,
scope,
+ client,
)) as FeedbackEvent | null;
- // If e.g. a global event processor returned null
- if (!preparedEvent) {
+ if (preparedEvent === null) {
+ // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
+ client.recordDroppedEvent('event_processor', 'feedback', event);
return null;
}
@@ -39,14 +39,5 @@ export async function prepareFeedbackEvent({
// we need to do this manually.
preparedEvent.platform = preparedEvent.platform || 'javascript';
- // extract the SDK name because `client._prepareEvent` doesn't add it to the event
- const metadata = client.getSdkMetadata && client.getSdkMetadata();
- const { name, version } = (metadata && metadata.sdk) || {};
-
- preparedEvent.sdk = {
- ...preparedEvent.sdk,
- name: name || 'sentry.javascript.unknown',
- version: version || '0.0.0',
- };
return preparedEvent;
}
diff --git a/packages/feedback/src/util/sendFeedbackRequest.ts b/packages/feedback/src/util/sendFeedbackRequest.ts
index 626457d6122b..45a3bd493de9 100644
--- a/packages/feedback/src/util/sendFeedbackRequest.ts
+++ b/packages/feedback/src/util/sendFeedbackRequest.ts
@@ -1,39 +1,36 @@
-import { getCurrentHub } from '@sentry/core';
-import { dsnToString } from '@sentry/utils';
+import { createEventEnvelope, getCurrentHub } from '@sentry/core';
+import type { FeedbackEvent, TransportMakeRequestResponse } from '@sentry/types';
import type { SendFeedbackData } from '../types';
import { prepareFeedbackEvent } from './prepareFeedbackEvent';
/**
- * Send feedback using `fetch()`
+ * Send feedback using transport
*/
export async function sendFeedbackRequest({
feedback: { message, email, name, replay_id, url },
-}: SendFeedbackData): Promise {
+}: SendFeedbackData): Promise {
const hub = getCurrentHub();
-
- if (!hub) {
- return null;
- }
-
const client = hub.getClient();
const scope = hub.getScope();
const transport = client && client.getTransport();
const dsn = client && client.getDsn();
if (!client || !transport || !dsn) {
- return null;
+ return;
}
- const baseEvent = {
- feedback: {
- contact_email: email,
- name,
- message,
- replay_id,
- url,
+ const baseEvent: FeedbackEvent = {
+ contexts: {
+ feedback: {
+ contact_email: email,
+ name,
+ message,
+ replay_id,
+ url,
+ },
},
- // type: 'feedback_event',
+ type: 'feedback',
};
const feedbackEvent = await prepareFeedbackEvent({
@@ -42,72 +39,80 @@ export async function sendFeedbackRequest({
event: baseEvent,
});
- if (!feedbackEvent) {
- // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
- // client.recordDroppedEvent('event_processor', 'feedback', baseEvent);
- return null;
+ if (feedbackEvent === null) {
+ return;
}
/*
For reference, the fully built event looks something like this:
{
- "data": {
- "dist": "abc123",
+ "type": "feedback",
+ "event_id": "d2132d31b39445f1938d7e21b6bf0ec4",
+ "timestamp": 1597977777.6189718,
+ "dist": "1.12",
+ "platform": "javascript",
"environment": "production",
- "feedback": {
- "contact_email": "colton.allen@sentry.io",
- "message": "I really like this user-feedback feature!",
- "replay_id": "ec3b4dc8b79f417596f7a1aa4fcca5d2",
- "url": "https://docs.sentry.io/platforms/javascript/"
+ "release": 42,
+ "tags": {"transaction": "/organizations/:orgId/performance/:eventSlug/"},
+ "sdk": {"name": "name", "version": "version"},
+ "user": {
+ "id": "123",
+ "username": "user",
+ "email": "user@site.com",
+ "ip_address": "192.168.11.12",
},
- "id": "1ffe0775ac0f4417aed9de36d9f6f8dc",
- "platform": "javascript",
- "release": "version@1.3",
"request": {
- "headers": {
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
- }
- },
- "sdk": {
- "name": "sentry.javascript.react",
- "version": "6.18.1"
+ "url": None,
+ "headers": {
+ "user-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15"
+ },
},
- "tags": {
- "key": "value"
+ "contexts": {
+ "feedback": {
+ "message": "test message",
+ "contact_email": "test@example.com",
+ "type": "feedback",
+ },
+ "trace": {
+ "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
+ "span_id": "FA90FDEAD5F74052",
+ "type": "trace",
+ },
+ "replay": {
+ "replay_id": "e2d42047b1c5431c8cba85ee2a8ab25d",
+ },
},
- "timestamp": "2023-08-31T14:10:34.954048",
- "user": {
- "email": "username@example.com",
- "id": "123",
- "ip_address": "127.0.0.1",
- "name": "user",
- "username": "user2270129"
- }
}
- }
*/
- // Prevent this data (which, if it exists, was used in earlier steps in the processing pipeline) from being sent to
- // sentry. (Note: Our use of this property comes and goes with whatever we might be debugging, whatever hacks we may
- // have temporarily added, etc. Even if we don't happen to be using it at some point in the future, let's not get rid
- // of this `delete`, lest we miss putting it back in the next time the property is in use.)
- delete feedbackEvent.sdkProcessingMetadata;
+ const envelope = createEventEnvelope(feedbackEvent, dsn, client.getOptions()._metadata, client.getOptions().tunnel);
+
+ let response: void | TransportMakeRequestResponse;
try {
- const path = 'https://sentry.io/api/0/feedback/';
- const response = await fetch(path, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `DSN ${dsnToString(dsn)}`,
- },
- body: JSON.stringify(feedbackEvent),
- });
- if (!response.ok) {
- return null;
+ response = await transport.send(envelope);
+ } catch (err) {
+ const error = new Error('Unable to send Feedback');
+
+ try {
+ // In case browsers don't allow this property to be writable
+ // @ts-expect-error This needs lib es2022 and newer
+ error.cause = err;
+ } catch {
+ // nothing to do
}
+ throw error;
+ }
+
+ // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore
+ if (!response) {
return response;
- } catch (err) {
- return null;
}
+
+ // Require valid status codes, otherwise can assume feedback was not sent successfully
+ if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
+ throw new Error('Unable to send Feedback');
+ }
+
+ return response;
}
diff --git a/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts b/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts
index cee0d99f0b5c..c5a2c37390fb 100644
--- a/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts
+++ b/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts
@@ -1,8 +1,7 @@
import type { Hub, Scope } from '@sentry/core';
import { getCurrentHub } from '@sentry/core';
-import type { Client } from '@sentry/types';
+import type { Client, FeedbackEvent } from '@sentry/types';
-import type { FeedbackEvent } from '../../../src/types';
import { prepareFeedbackEvent } from '../../../src/util/prepareFeedbackEvent';
import { getDefaultClientOptions, TestClient } from '../../utils/TestClient';
@@ -18,15 +17,6 @@ describe('Unit | util | prepareFeedbackEvent', () => {
client = hub.getClient()!;
scope = hub.getScope()!;
-
- jest.spyOn(client, 'getSdkMetadata').mockImplementation(() => {
- return {
- sdk: {
- name: 'sentry.javascript.testSdk',
- version: '1.0.0',
- },
- };
- });
});
afterEach(() => {
@@ -41,13 +31,14 @@ describe('Unit | util | prepareFeedbackEvent', () => {
const event: FeedbackEvent = {
timestamp: 1670837008.634,
event_id: 'feedback-ID',
- feedback: {
- contact_email: 'test@test.com',
- message: 'looks great!',
- replay_id: replayId,
- url: 'https://sentry.io/',
- },
+ type: 'feedback',
contexts: {
+ feedback: {
+ contact_email: 'test@test.com',
+ message: 'looks great!',
+ replay_id: replayId,
+ url: 'https://sentry.io/',
+ },
replay: {
error_sample_rate: 1.0,
session_sample_rate: 0.1,
@@ -57,31 +48,26 @@ describe('Unit | util | prepareFeedbackEvent', () => {
const feedbackEvent = await prepareFeedbackEvent({ scope, client, event });
- expect(client.getSdkMetadata).toHaveBeenCalledTimes(1);
-
expect(feedbackEvent).toEqual({
timestamp: 1670837008.634,
event_id: 'feedback-ID',
- feedback: {
- contact_email: 'test@test.com',
- message: 'looks great!',
- replay_id: replayId,
- url: 'https://sentry.io/',
- },
platform: 'javascript',
environment: 'production',
contexts: {
+ feedback: {
+ contact_email: 'test@test.com',
+ message: 'looks great!',
+ replay_id: replayId,
+ url: 'https://sentry.io/',
+ },
replay: {
error_sample_rate: 1.0,
session_sample_rate: 0.1,
},
},
- sdk: {
- name: 'sentry.javascript.testSdk',
- version: '1.0.0',
- },
sdkProcessingMetadata: expect.any(Object),
breadcrumbs: undefined,
+ type: 'feedback',
});
});
});
diff --git a/packages/feedback/test/widget/createWidget.test.ts b/packages/feedback/test/widget/createWidget.test.ts
index 1a18b5b93605..1c39de8f26c1 100644
--- a/packages/feedback/test/widget/createWidget.test.ts
+++ b/packages/feedback/test/widget/createWidget.test.ts
@@ -177,7 +177,7 @@ describe('createWidget', () => {
});
(sendFeedbackRequest as jest.Mock).mockImplementation(() => {
- return false;
+ throw new Error('Unable to send feedback');
});
widget.actor?.el?.dispatchEvent(new Event('click'));
diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
index a4d7a96b94fd..42fecd1d39ff 100644
--- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
@@ -1,4 +1,4 @@
-import { addTracingExtensions, captureException, flush, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core';
+import { captureException, flush, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core';
import { addExceptionMechanism, tracingContextFromHeaders } from '@sentry/utils';
import { isRedirectNavigationError } from './nextNavigationErrorUtils';
@@ -13,8 +13,6 @@ export function wrapRouteHandlerWithSentry any>(
routeHandler: F,
context: RouteHandlerContext,
): (...args: Parameters) => ReturnType extends Promise ? ReturnType : Promise> {
- addTracingExtensions();
-
const { method, parameterizedRoute, baggageHeader, sentryTraceHeader } = context;
return new Proxy(routeHandler, {
diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
index 360b169c7b37..6efa50d2b804 100644
--- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
+++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
@@ -1,11 +1,4 @@
-import {
- addTracingExtensions,
- captureException,
- flush,
- getCurrentHub,
- runWithAsyncContext,
- startTransaction,
-} from '@sentry/core';
+import { captureException, flush, getCurrentHub, runWithAsyncContext, startTransaction } from '@sentry/core';
import { addExceptionMechanism, tracingContextFromHeaders } from '@sentry/utils';
import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils';
@@ -19,8 +12,6 @@ export function wrapServerComponentWithSentry any>
appDirComponent: F,
context: ServerComponentContext,
): F {
- addTracingExtensions();
-
const { componentRoute, componentType } = context;
// Even though users may define server components as async functions, for the client bundles
diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts
index 07c6de85e92c..7c66d8f1fb7f 100644
--- a/packages/nextjs/src/edge/index.ts
+++ b/packages/nextjs/src/edge/index.ts
@@ -1,10 +1,12 @@
-import { SDK_VERSION } from '@sentry/core';
+import { addTracingExtensions, SDK_VERSION } from '@sentry/core';
import { RewriteFrames } from '@sentry/integrations';
import type { SdkMetadata } from '@sentry/types';
import { addOrUpdateIntegration, escapeStringForRegex, GLOBAL_OBJ } from '@sentry/utils';
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
import { init as vercelEdgeInit } from '@sentry/vercel-edge';
+import { isBuild } from '../common/utils/isBuild';
+
export type EdgeOptions = VercelEdgeOptions;
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
@@ -13,6 +15,12 @@ const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
/** Inits the Sentry NextJS SDK on the Edge Runtime. */
export function init(options: VercelEdgeOptions = {}): void {
+ addTracingExtensions();
+
+ if (isBuild()) {
+ return;
+ }
+
const opts = {
_metadata: {} as SdkMetadata,
...options,
diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts
index cfbe245ca936..822f2619d127 100644
--- a/packages/nextjs/src/server/index.ts
+++ b/packages/nextjs/src/server/index.ts
@@ -1,3 +1,4 @@
+import { addTracingExtensions } from '@sentry/core';
import { RewriteFrames } from '@sentry/integrations';
import type { NodeOptions } from '@sentry/node';
import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node';
@@ -63,6 +64,12 @@ const IS_VERCEL = !!process.env.VERCEL;
/** Inits the Sentry NextJS SDK on node. */
export function init(options: NodeOptions): void {
+ addTracingExtensions();
+
+ if (isBuild()) {
+ return;
+ }
+
const opts = {
environment: process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV,
...options,
diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts
index 868cf7d5a6b2..f080b00275dc 100644
--- a/packages/node/src/handlers.ts
+++ b/packages/node/src/handlers.ts
@@ -338,7 +338,7 @@ interface TrpcMiddlewareArguments {
* e.g. Express Request Handlers or Next.js SDK.
*/
export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) {
- return function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): T {
+ return async function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): Promise {
const hub = getCurrentHub();
const clientOptions = hub.getClient()?.getOptions();
const sentryTransaction = hub.getScope().getTransaction();
@@ -358,7 +358,36 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) {
sentryTransaction.setContext('trpc', trpcContext);
}
- return next();
+ function captureError(e: unknown): void {
+ captureException(e, scope => {
+ scope.addEventProcessor(event => {
+ addExceptionMechanism(event, {
+ handled: false,
+ });
+ return event;
+ });
+
+ return scope;
+ });
+ }
+
+ try {
+ return await next();
+ } catch (e: unknown) {
+ if (typeof e === 'object' && e) {
+ if ('code' in e) {
+ // Is likely TRPCError - we only want to capture internal server errors
+ if (e.code === 'INTERNAL_SERVER_ERROR') {
+ captureError(e);
+ }
+ } else {
+ // Is likely random error that bubbles up
+ captureError(e);
+ }
+ }
+
+ throw e;
+ }
};
}
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index 42034876d269..7686a08782c8 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -62,6 +62,8 @@ export { remixRouterInstrumentation, withSentry } from './client/performance';
export { captureRemixErrorBoundaryError } from './client/errors';
export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
+export type { SentryMetaArgs } from './utils/types';
+
function sdkAlreadyInitialized(): boolean {
const hub = getCurrentHub();
return !!hub.getClient();
diff --git a/packages/remix/src/utils/types.ts b/packages/remix/src/utils/types.ts
new file mode 100644
index 000000000000..9634678a05ce
--- /dev/null
+++ b/packages/remix/src/utils/types.ts
@@ -0,0 +1,6 @@
+export type SentryMetaArgs any> = Parameters[0] & {
+ data: {
+ sentryTrace: string;
+ sentryBaggage: string;
+ };
+};
diff --git a/packages/types/src/datacategory.ts b/packages/types/src/datacategory.ts
index 84340cffc4f1..ca2acd29e235 100644
--- a/packages/types/src/datacategory.ts
+++ b/packages/types/src/datacategory.ts
@@ -24,5 +24,7 @@ export type DataCategory =
| 'profile'
// Check-in event (monitor)
| 'monitor'
+ // Feedback type event (v2)
+ | 'feedback'
// Unknown data category
| 'unknown';
diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts
index 051de2a07960..d479b9f8a4e9 100644
--- a/packages/types/src/envelope.ts
+++ b/packages/types/src/envelope.ts
@@ -2,6 +2,7 @@ import type { SerializedCheckIn } from './checkin';
import type { ClientReport } from './clientreport';
import type { DsnComponents } from './dsn';
import type { Event } from './event';
+import type { FeedbackEvent } from './feedback';
import type { ReplayEvent, ReplayRecordingData } from './replay';
import type { SdkInfo } from './sdkinfo';
import type { SerializedSession, Session, SessionAggregates } from './session';
@@ -26,6 +27,7 @@ export type DynamicSamplingContext = {
export type EnvelopeItemType =
| 'client_report'
| 'user_report'
+ | 'feedback'
| 'session'
| 'sessions'
| 'transaction'
@@ -57,7 +59,7 @@ type BaseEnvelope = [
];
type EventItemHeaders = {
- type: 'event' | 'transaction' | 'profile';
+ type: 'event' | 'transaction' | 'profile' | 'feedback';
};
type AttachmentItemHeaders = {
type: 'attachment';
@@ -67,6 +69,7 @@ type AttachmentItemHeaders = {
attachment_type?: string;
};
type UserFeedbackItemHeaders = { type: 'user_report' };
+type FeedbackItemHeaders = { type: 'feedback' };
type SessionItemHeaders = { type: 'session' };
type SessionAggregatesItemHeaders = { type: 'sessions' };
type ClientReportItemHeaders = { type: 'client_report' };
@@ -87,6 +90,7 @@ export type CheckInItem = BaseEnvelopeItem;
type ReplayRecordingItem = BaseEnvelopeItem;
export type StatsdItem = BaseEnvelopeItem;
+export type FeedbackItem = BaseEnvelopeItem;
export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext };
type SessionEnvelopeHeaders = { sent_at: string };
@@ -95,7 +99,10 @@ type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders;
type ReplayEnvelopeHeaders = BaseEnvelopeHeaders;
type StatsdEnvelopeHeaders = BaseEnvelopeHeaders;
-export type EventEnvelope = BaseEnvelope;
+export type EventEnvelope = BaseEnvelope<
+ EventEnvelopeHeaders,
+ EventItem | AttachmentItem | UserFeedbackItem | FeedbackItem
+>;
export type SessionEnvelope = BaseEnvelope;
export type ClientReportEnvelope = BaseEnvelope;
export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRecordingItem]];
diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts
index 61783ef9361b..55207f89337d 100644
--- a/packages/types/src/event.ts
+++ b/packages/types/src/event.ts
@@ -61,7 +61,7 @@ export interface Event {
* Note that `ErrorEvent`s do not have a type (hence its undefined),
* while all other events are required to have one.
*/
-export type EventType = 'transaction' | 'profile' | 'replay_event' | undefined;
+export type EventType = 'transaction' | 'profile' | 'replay_event' | 'feedback' | undefined;
export interface ErrorEvent extends Event {
type: undefined;
diff --git a/packages/types/src/feedback.ts b/packages/types/src/feedback.ts
new file mode 100644
index 000000000000..360064cda833
--- /dev/null
+++ b/packages/types/src/feedback.ts
@@ -0,0 +1,20 @@
+import type { Event } from './event';
+
+export interface FeedbackContext extends Record {
+ message: string;
+ contact_email?: string;
+ name?: string;
+ replay_id?: string;
+ url?: string;
+}
+
+/**
+ * NOTE: These types are still considered Alpha and subject to change.
+ * @hidden
+ */
+export interface FeedbackEvent extends Event {
+ type: 'feedback';
+ contexts: Event['contexts'] & {
+ feedback: FeedbackContext;
+ };
+}
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index a8a9531a6490..27068f8080f8 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -36,6 +36,7 @@ export type {
EventEnvelopeHeaders,
EventItem,
ReplayEnvelope,
+ FeedbackItem,
SessionEnvelope,
SessionItem,
UserFeedbackItem,
@@ -69,6 +70,7 @@ export type {
Profile,
} from './profiling';
export type { ReplayEvent, ReplayRecordingData, ReplayRecordingMode } from './replay';
+export type { FeedbackEvent } from './feedback';
export type { QueryParams, Request, SanitizedRequestData } from './request';
export type { Runtime } from './runtime';
export type { CaptureContext, Scope, ScopeContext } from './scope';
diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts
index 54772c6ae6fc..c716ab282dfe 100644
--- a/packages/utils/src/envelope.ts
+++ b/packages/utils/src/envelope.ts
@@ -208,6 +208,7 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = {
replay_event: 'replay',
replay_recording: 'replay',
check_in: 'monitor',
+ feedback: 'feedback',
// TODO: This is a temporary workaround until we have a proper data category for metrics
statsd: 'unknown',
};