Skip to content

Commit df5f44e

Browse files
c298leebillyvgryan953AbhiPrasad
authored
feat(feedback): New feedback integration with screenshots (#10590)
Replace the current screenshot package with a new package that has screenshots and uses Preact for better readability. It will also allow for lazy loading in the future. Co-authored-by: Billy Vong <[email protected]> Co-authored-by: Ryan Albrecht <[email protected]> Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent fc208fe commit df5f44e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2104
-2757
lines changed

.size-limit.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,26 @@ module.exports = [
5555
limit: '35 KB',
5656
},
5757
{
58-
name: '@sentry/browser (incl. Feedback) - Webpack (gzipped)',
58+
name: '@sentry/browser (incl. feedbackIntegration) - Webpack (gzipped)',
5959
path: 'packages/browser/build/npm/esm/index.js',
6060
import: '{ init, feedbackIntegration }',
6161
gzip: true,
6262
limit: '50 KB',
6363
},
64+
{
65+
name: '@sentry/browser (incl. feedbackModalIntegration) - Webpack (gzipped)',
66+
path: 'packages/browser/build/npm/esm/index.js',
67+
import: '{ init, feedbackIntegration, feedbackModalIntegration }',
68+
gzip: true,
69+
limit: '50 KB',
70+
},
71+
{
72+
name: '@sentry/browser (incl. feedbackScreenshotIntegration) - Webpack (gzipped)',
73+
path: 'packages/browser/build/npm/esm/index.js',
74+
import: '{ init, feedbackIntegration, feedbackModalIntegration, feedbackScreenshotIntegration }',
75+
gzip: true,
76+
limit: '50 KB',
77+
},
6478
{
6579
name: '@sentry/browser (incl. sendFeedback) - Webpack (gzipped)',
6680
path: 'packages/browser/build/npm/esm/index.js',

packages/core/src/envelope.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {
2+
Attachment,
3+
AttachmentItem,
24
DsnComponents,
35
Event,
46
EventEnvelope,
@@ -11,6 +13,7 @@ import type {
1113
SessionItem,
1214
} from '@sentry/types';
1315
import {
16+
createAttachmentEnvelopeItem,
1417
createEnvelope,
1518
createEventEnvelopeHeaders,
1619
dsnToString,
@@ -86,3 +89,31 @@ export function createEventEnvelope(
8689
const eventItem: EventItem = [{ type: eventType }, event];
8790
return createEnvelope<EventEnvelope>(envelopeHeaders, [eventItem]);
8891
}
92+
93+
/**
94+
* Create an Envelope from an event.
95+
*/
96+
export function createAttachmentEnvelope(
97+
event: Event,
98+
attachments: Attachment[],
99+
dsn?: DsnComponents,
100+
metadata?: SdkMetadata,
101+
tunnel?: string,
102+
): EventEnvelope {
103+
const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata);
104+
enhanceEventWithSdkInfo(event, metadata && metadata.sdk);
105+
106+
const envelopeHeaders = createEventEnvelopeHeaders(event, sdkInfo, tunnel, dsn);
107+
108+
// Prevent this data (which, if it exists, was used in earlier steps in the processing pipeline) from being sent to
109+
// sentry. (Note: Our use of this property comes and goes with whatever we might be debugging, whatever hacks we may
110+
// 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
111+
// of this `delete`, lest we miss putting it back in the next time the property is in use.)
112+
delete event.sdkProcessingMetadata;
113+
114+
const attachmentItems: AttachmentItem[] = [];
115+
for (const attachment of attachments || []) {
116+
attachmentItems.push(createAttachmentEnvelopeItem(attachment));
117+
}
118+
return createEnvelope<EventEnvelope>(envelopeHeaders, attachmentItems);
119+
}

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type { IntegrationIndex } from './integration';
88

99
export * from './tracing';
1010
export * from './semanticAttributes';
11-
export { createEventEnvelope, createSessionEnvelope } from './envelope';
11+
export { createEventEnvelope, createSessionEnvelope, createAttachmentEnvelope } from './envelope';
1212
export {
1313
captureCheckIn,
1414
withMonitor,

packages/feedback/.eslintrc.js

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,6 @@ module.exports = {
1111
parserOptions: {
1212
project: ['tsconfig.test.json'],
1313
},
14-
rules: {
15-
'no-console': 'off',
16-
},
17-
},
18-
{
19-
files: ['test/**/*.ts'],
20-
21-
rules: {
22-
// most of these errors come from `new Promise(process.nextTick)`
23-
'@typescript-eslint/unbound-method': 'off',
24-
// TODO: decide if we want to enable this again after the migration
25-
// We can take the freedom to be a bit more lenient with tests
26-
'@typescript-eslint/no-floating-promises': 'off',
27-
},
28-
},
29-
{
30-
files: ['src/types/deprecated.ts'],
31-
rules: {
32-
'@typescript-eslint/naming-convention': 'off',
33-
},
3414
},
3515
],
3616
};

packages/feedback/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"dependencies": {
4545
"@sentry/core": "8.0.0-alpha.2",
4646
"@sentry/types": "8.0.0-alpha.2",
47-
"@sentry/utils": "8.0.0-alpha.2"
47+
"@sentry/utils": "8.0.0-alpha.2",
48+
"preact": "^10.19.4"
4849
},
4950
"scripts": {
5051
"build": "run-p build:transpile build:types build:bundle",
@@ -53,7 +54,7 @@
5354
"build:dev": "run-p build:transpile build:types",
5455
"build:types": "run-s build:types:core build:types:downlevel",
5556
"build:types:core": "tsc -p tsconfig.types.json",
56-
"build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8",
57+
"build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8 && yarn node ./scripts/shim-preact-export.js",
5758
"build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch",
5859
"build:dev:watch": "run-p build:transpile:watch build:types:watch",
5960
"build:transpile:watch": "yarn build:transpile --watch",
Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils';
22

3-
const baseBundleConfig = makeBaseBundleConfig({
4-
bundleType: 'addon',
5-
entrypoints: ['src/index.ts'],
6-
licenseTitle: '@sentry-internal/feedback',
7-
outputFileBase: () => 'bundles/feedback',
8-
});
9-
10-
const builds = makeBundleConfigVariants(baseBundleConfig);
11-
12-
export default builds;
3+
export default makeBundleConfigVariants(
4+
makeBaseBundleConfig({
5+
bundleType: 'addon',
6+
entrypoints: ['src/index.ts'],
7+
jsVersion: 'es6',
8+
licenseTitle: '@sentry-internal/feedback',
9+
outputFileBase: () => 'bundles/feedback',
10+
sucrase: {
11+
jsxPragma: 'h',
12+
jsxFragmentPragma: 'Fragment',
13+
},
14+
}),
15+
);

packages/feedback/rollup.npm.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,9 @@ export default makeNPMConfigVariants(
1212
preserveModules: false,
1313
},
1414
},
15+
sucrase: {
16+
jsxPragma: 'h',
17+
jsxFragmentPragma: 'Fragment',
18+
},
1519
}),
1620
);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// preact does not support more modern TypeScript versions, which breaks our users that depend on older
2+
// TypeScript versions. To fix this, we shim the types from preact to be any and remove the dependency on preact
3+
// for types directly. This script is meant to be run after the build/npm/types-ts3.8 directory is created.
4+
5+
// Path: build/npm/types-ts3.8/global.d.ts
6+
7+
const fs = require('fs');
8+
const path = require('path');
9+
10+
/**
11+
* This regex looks for preact imports we can replace and shim out.
12+
*
13+
* Example:
14+
* import { ComponentChildren, VNode } from 'preact';
15+
*/
16+
const preactImportRegex = /import\s*{\s*([\w\s,]+)\s*}\s*from\s*'preact'\s*;?/;
17+
18+
function walk(dir) {
19+
const files = fs.readdirSync(dir);
20+
files.forEach(file => {
21+
const filePath = path.join(dir, file);
22+
const stat = fs.lstatSync(filePath);
23+
if (stat.isDirectory()) {
24+
walk(filePath);
25+
} else {
26+
if (filePath.endsWith('.d.ts')) {
27+
const content = fs.readFileSync(filePath, 'utf8');
28+
const capture = preactImportRegex.exec(content);
29+
if (capture) {
30+
const groups = capture[1].split(',').map(s => s.trim());
31+
32+
// This generates a shim snippet to replace the type imports from preact
33+
// It generates a snippet based on the capture groups of preactImportRegex.
34+
//
35+
// Example:
36+
//
37+
// import type { ComponentChildren, VNode } from 'preact';
38+
// becomes
39+
// type ComponentChildren: any;
40+
// type VNode: any;
41+
const snippet = groups.reduce((acc, curr) => {
42+
const searchableValue = curr.includes(' as ') ? curr.split(' as ')[1] : curr;
43+
44+
// look to see if imported as value, then we have to use declare const
45+
if (content.includes(`typeof ${searchableValue}`)) {
46+
return `${acc}declare const ${searchableValue}: any;\n`;
47+
}
48+
49+
// look to see if generic type like Foo<T>
50+
if (content.includes(`${searchableValue}<`)) {
51+
return `${acc}type ${searchableValue}<T> = any;\n`;
52+
}
53+
54+
// otherwise we can just leave as type
55+
return `${acc}type ${searchableValue} = any;\n`;
56+
}, '');
57+
58+
// we then can remove the import from preact
59+
const newContent = content.replace(preactImportRegex, '// replaced import from preact');
60+
61+
// and write the new content to the file
62+
fs.writeFileSync(filePath, snippet + newContent, 'utf8');
63+
}
64+
}
65+
}
66+
});
67+
}
68+
69+
function run() {
70+
// recurse through build/npm/types-ts3.8 directory
71+
const dir = path.join('build', 'npm', 'types-ts3.8');
72+
walk(dir);
73+
}
74+
75+
run();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { GLOBAL_OBJ } from '@sentry/utils';
2+
3+
export { DEFAULT_THEME } from './theme';
4+
5+
// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser`
6+
// prevents the browser package from being bundled in the CDN bundle, and avoids a
7+
// circular dependency between the browser and feedback packages
8+
export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
9+
export const DOCUMENT = WINDOW.document;
10+
export const NAVIGATOR = WINDOW.navigator;
11+
12+
export const ACTOR_LABEL = 'Report a Bug';
13+
export const CANCEL_BUTTON_LABEL = 'Cancel';
14+
export const SUBMIT_BUTTON_LABEL = 'Send Bug Report';
15+
export const FORM_TITLE = 'Report a Bug';
16+
export const EMAIL_PLACEHOLDER = '[email protected]';
17+
export const EMAIL_LABEL = 'Email';
18+
export const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?";
19+
export const MESSAGE_LABEL = 'Description';
20+
export const NAME_PLACEHOLDER = 'Your Name';
21+
export const NAME_LABEL = 'Name';
22+
export const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!';
23+
24+
export const FEEDBACK_WIDGET_SOURCE = 'widget';
25+
export const FEEDBACK_API_SOURCE = 'api';
26+
27+
export const SUCCESS_MESSAGE_TIMEOUT = 5000;

packages/feedback/src/constants.ts renamed to packages/feedback/src/constants/theme.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import { GLOBAL_OBJ } from '@sentry/utils';
2-
3-
// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser`
4-
// prevents the browser package from being bundled in the CDN bundle, and avoids a
5-
// circular dependency between the browser and feedback packages
6-
export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
7-
81
const LIGHT_BACKGROUND = '#ffffff';
92
const INHERIT = 'inherit';
103
const SUBMIT_COLOR = 'rgba(108, 95, 199, 1)';
11-
const LIGHT_THEME = {
4+
5+
export const LIGHT_THEME = {
126
fontFamily: "system-ui, 'Helvetica Neue', Arial, sans-serif",
137
fontSize: '14px',
148

@@ -59,18 +53,3 @@ export const DEFAULT_THEME = {
5953
error: '#f55459',
6054
},
6155
};
62-
63-
export const ACTOR_LABEL = 'Report a Bug';
64-
export const CANCEL_BUTTON_LABEL = 'Cancel';
65-
export const SUBMIT_BUTTON_LABEL = 'Send Bug Report';
66-
export const FORM_TITLE = 'Report a Bug';
67-
export const EMAIL_PLACEHOLDER = '[email protected]';
68-
export const EMAIL_LABEL = 'Email';
69-
export const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?";
70-
export const MESSAGE_LABEL = 'Description';
71-
export const NAME_PLACEHOLDER = 'Your Name';
72-
export const NAME_LABEL = 'Name';
73-
export const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!';
74-
75-
export const FEEDBACK_WIDGET_SOURCE = 'widget';
76-
export const FEEDBACK_API_SOURCE = 'api';

packages/feedback/test/utils/TestClient.ts renamed to packages/feedback/src/core/TestClient.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,50 @@ import { resolvedSyncPromise } from '@sentry/utils';
44

55
export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {}
66

7+
/**
8+
*
9+
*/
710
export class TestClient extends BaseClient<TestClientOptions> {
811
public constructor(options: TestClientOptions) {
912
super(options);
1013
}
1114

15+
/**
16+
*
17+
*/
1218
public eventFromException(exception: any): PromiseLike<Event> {
1319
return resolvedSyncPromise({
1420
exception: {
1521
values: [
1622
{
23+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1724
type: exception.name,
25+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1826
value: exception.message,
19-
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
2027
},
2128
],
2229
},
2330
});
2431
}
2532

33+
/**
34+
*
35+
*/
2636
public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike<Event> {
2737
return resolvedSyncPromise({ message, level });
2838
}
2939
}
3040

41+
/**
42+
*
43+
*/
3144
export function init(options: TestClientOptions): void {
3245
initAndBind(TestClient, options);
3346
}
3447

48+
/**
49+
*
50+
*/
3551
export function getDefaultClientOptions(options: Partial<ClientOptions> = {}): ClientOptions {
3652
return {
3753
integrations: [],

packages/feedback/src/widget/Actor.css.ts renamed to packages/feedback/src/core/components/Actor.css.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1+
import { DOCUMENT } from '../../constants';
2+
13
/**
24
* Creates <style> element for widget actor (button that opens the dialog)
35
*/
4-
export function createActorStyles(d: Document): HTMLStyleElement {
5-
const style = d.createElement('style');
6+
export function createActorStyles(): HTMLStyleElement {
7+
const style = DOCUMENT.createElement('style');
68
style.textContent = `
79
.widget__actor {
10+
position: fixed;
11+
left: var(--left);
12+
right: var(--right);
13+
bottom: var(--bottom);
14+
top: var(--top);
15+
z-index: var(--z-index);
16+
817
line-height: 25px;
918
1019
display: flex;
1120
align-items: center;
1221
gap: 8px;
1322
23+
1424
border-radius: var(--border-radius);
1525
cursor: pointer;
1626
font-size: 14px;

0 commit comments

Comments
 (0)