Skip to content

Commit 5a06fd1

Browse files
committed
add sendfeedback to sdk
1 parent 1e92868 commit 5a06fd1

File tree

8 files changed

+184
-22
lines changed

8 files changed

+184
-22
lines changed

packages/feedback/src/index.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface SendFeedbackData {
2+
message: string,
3+
email: string,
4+
replay_id: string,
5+
url: string,
6+
}

packages/feedback/src/types/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
export * from './performance';
2-
export * from './replay';
3-
export * from './replayFrame';
4-
export * from './request';
5-
export * from './rrweb';
1+
export * from './feedback';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type {Scope} from '@sentry/core';
2+
import {prepareEvent} from '@sentry/core';
3+
import type {Client, FeedbackEvent} from '@sentry/types';
4+
5+
/**
6+
* Prepare a feedback event & enrich it with the SDK metadata.
7+
*/
8+
export async function prepareFeedbackEvent({
9+
client,
10+
scope,
11+
event,
12+
}: {
13+
client: Client;
14+
event: FeedbackEvent;
15+
scope: Scope;
16+
}): Promise<FeedbackEvent | null> {
17+
const preparedEvent = (await prepareEvent(
18+
client.getOptions(),
19+
event,
20+
{integrations: []},
21+
scope
22+
)) as FeedbackEvent | null;
23+
24+
// If e.g. a global event processor returned null
25+
if (!preparedEvent) {
26+
return null;
27+
}
28+
29+
// This normally happens in browser client "_prepareEvent"
30+
// but since we do not use this private method from the client, but rather the plain import
31+
// we need to do this manually.
32+
preparedEvent.platform = preparedEvent.platform || 'javascript';
33+
34+
// extract the SDK name because `client._prepareEvent` doesn't add it to the event
35+
const metadata = client.getSdkMetadata && client.getSdkMetadata();
36+
const {name, version} = (metadata && metadata.sdk) || {};
37+
38+
preparedEvent.sdk = {
39+
...preparedEvent.sdk,
40+
name: name || 'sentry.javascript.unknown',
41+
version: version || '0.0.0',
42+
};
43+
return preparedEvent;
44+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { getCurrentHub } from '@sentry/core';
2+
import { dsnToString } from '@sentry/utils';
3+
4+
import type { SendFeedbackData } from '../types';
5+
import { prepareFeedbackEvent } from './prepareFeedbackEvent';
6+
7+
/**
8+
* Send feedback using `fetch()`
9+
*/
10+
export async function sendFeedbackRequest({
11+
message,
12+
email,
13+
replay_id,
14+
url,
15+
}: SendFeedbackData): Promise<Response | null> {
16+
const hub = getCurrentHub();
17+
18+
if (!hub) {
19+
return null;
20+
}
21+
22+
const client = hub.getClient();
23+
const scope = hub.getScope();
24+
const transport = client && client.getTransport();
25+
const dsn = client && client.getDsn();
26+
27+
if (!client || !transport || !dsn) {
28+
return null;
29+
}
30+
31+
const baseEvent = {
32+
feedback: {
33+
contact_email: email,
34+
message,
35+
replay_id,
36+
url,
37+
},
38+
// type: 'feedback_event',
39+
};
40+
41+
const feedbackEvent = await prepareFeedbackEvent({
42+
scope,
43+
client,
44+
event: baseEvent,
45+
});
46+
47+
if (!feedbackEvent) {
48+
// Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
49+
// client.recordDroppedEvent('event_processor', 'feedback', baseEvent);
50+
return null;
51+
}
52+
53+
/*
54+
For reference, the fully built event looks something like this:
55+
{
56+
"data": {
57+
"dist": "abc123",
58+
"environment": "production",
59+
"feedback": {
60+
"contact_email": "colton.allen@sentry.io",
61+
"message": "I really like this user-feedback feature!",
62+
"replay_id": "ec3b4dc8b79f417596f7a1aa4fcca5d2",
63+
"url": "https://docs.sentry.io/platforms/javascript/"
64+
},
65+
"id": "1ffe0775ac0f4417aed9de36d9f6f8dc",
66+
"platform": "javascript",
67+
"release": "[email protected]",
68+
"request": {
69+
"headers": {
70+
"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"
71+
}
72+
},
73+
"sdk": {
74+
"name": "sentry.javascript.react",
75+
"version": "6.18.1"
76+
},
77+
"tags": {
78+
"key": "value"
79+
},
80+
"timestamp": "2023-08-31T14:10:34.954048",
81+
"user": {
82+
"email": "username@example.com",
83+
"id": "123",
84+
"ip_address": "127.0.0.1",
85+
"name": "user",
86+
"username": "user2270129"
87+
}
88+
}
89+
}
90+
*/
91+
92+
// Prevent this data (which, if it exists, was used in earlier steps in the processing pipeline) from being sent to
93+
// sentry. (Note: Our use of this property comes and goes with whatever we might be debugging, whatever hacks we may
94+
// 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
95+
// of this `delete`, lest we miss putting it back in the next time the property is in use.)
96+
delete feedbackEvent.sdkProcessingMetadata;
97+
98+
try {
99+
const path = 'https://sentry.io/api/0/feedback/';
100+
const response = await fetch(path, {
101+
method: 'POST',
102+
headers: {
103+
'Content-Type': 'application/json',
104+
Authorization: `DSN ${dsnToString(dsn)}`,
105+
},
106+
body: JSON.stringify(feedbackEvent),
107+
});
108+
if (!response.ok) {
109+
return null;
110+
}
111+
return response;
112+
} catch (err) {
113+
return null;
114+
}
115+
}

packages/replay/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"compilerOptions": {
44
"module": "esnext"
55
},
6-
"include": ["src/**/*.ts"]
6+
"include": ["src/**/*.ts", "../feedback/src/util/sendFeedbackRequest.ts", "../feedback/src/types/feedback.ts"]
77
}

packages/types/src/feedback.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type {Event} from './event';
2+
3+
/**
4+
* NOTE: These types are still considered Beta and subject to change.
5+
* @hidden
6+
*/
7+
export interface FeedbackEvent extends Event {
8+
feedback: {
9+
contact_email: string;
10+
message: string;
11+
replay_id: string;
12+
url: string;
13+
};
14+
// TODO: Add this event type to Event
15+
// type: 'feedback_event';
16+
}

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export type {
6969
Profile,
7070
} from './profiling';
7171
export type { ReplayEvent, ReplayRecordingData, ReplayRecordingMode } from './replay';
72+
export type { FeedbackEvent } from './feedback';
7273
export type { QueryParams, Request, SanitizedRequestData } from './request';
7374
export type { Runtime } from './runtime';
7475
export type { CaptureContext, Scope, ScopeContext } from './scope';

0 commit comments

Comments
 (0)