diff --git a/.size-limit.js b/.size-limit.js index a9f5501c4635..c984f999e15a 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -55,12 +55,26 @@ module.exports = [ limit: '35 KB', }, { - name: '@sentry/browser (incl. Feedback) - Webpack (gzipped)', + name: '@sentry/browser (incl. feedbackIntegration) - Webpack (gzipped)', path: 'packages/browser/build/npm/esm/index.js', import: '{ init, feedbackIntegration }', gzip: true, limit: '50 KB', }, + { + name: '@sentry/browser (incl. feedbackModalIntegration) - Webpack (gzipped)', + path: 'packages/browser/build/npm/esm/index.js', + import: '{ init, feedbackIntegration, feedbackModalIntegration }', + gzip: true, + limit: '50 KB', + }, + { + name: '@sentry/browser (incl. feedbackScreenshotIntegration) - Webpack (gzipped)', + path: 'packages/browser/build/npm/esm/index.js', + import: '{ init, feedbackIntegration, feedbackModalIntegration, feedbackScreenshotIntegration }', + gzip: true, + limit: '50 KB', + }, { name: '@sentry/browser (incl. sendFeedback) - Webpack (gzipped)', path: 'packages/browser/build/npm/esm/index.js', diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 9ec29c9d2a7e..3189cdc5278a 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -1,4 +1,6 @@ import type { + Attachment, + AttachmentItem, DsnComponents, Event, EventEnvelope, @@ -11,6 +13,7 @@ import type { SessionItem, } from '@sentry/types'; import { + createAttachmentEnvelopeItem, createEnvelope, createEventEnvelopeHeaders, dsnToString, @@ -86,3 +89,31 @@ export function createEventEnvelope( const eventItem: EventItem = [{ type: eventType }, event]; return createEnvelope(envelopeHeaders, [eventItem]); } + +/** + * Create an Envelope from an event. + */ +export function createAttachmentEnvelope( + event: Event, + attachments: Attachment[], + dsn?: DsnComponents, + metadata?: SdkMetadata, + tunnel?: string, +): EventEnvelope { + const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata); + enhanceEventWithSdkInfo(event, metadata && metadata.sdk); + + const envelopeHeaders = createEventEnvelopeHeaders(event, sdkInfo, tunnel, dsn); + + // 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 event.sdkProcessingMetadata; + + const attachmentItems: AttachmentItem[] = []; + for (const attachment of attachments || []) { + attachmentItems.push(createAttachmentEnvelopeItem(attachment)); + } + return createEnvelope(envelopeHeaders, attachmentItems); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3fe4d4b3a59a..fbf233ad8dd8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,7 @@ export type { IntegrationIndex } from './integration'; export * from './tracing'; export * from './semanticAttributes'; -export { createEventEnvelope, createSessionEnvelope } from './envelope'; +export { createEventEnvelope, createSessionEnvelope, createAttachmentEnvelope } from './envelope'; export { captureCheckIn, withMonitor, diff --git a/packages/feedback/.eslintrc.js b/packages/feedback/.eslintrc.js index cf9985e769c0..0b547ffc828c 100644 --- a/packages/feedback/.eslintrc.js +++ b/packages/feedback/.eslintrc.js @@ -11,26 +11,6 @@ module.exports = { parserOptions: { project: ['tsconfig.test.json'], }, - rules: { - 'no-console': 'off', - }, - }, - { - files: ['test/**/*.ts'], - - rules: { - // most of these errors come from `new Promise(process.nextTick)` - '@typescript-eslint/unbound-method': 'off', - // TODO: decide if we want to enable this again after the migration - // We can take the freedom to be a bit more lenient with tests - '@typescript-eslint/no-floating-promises': 'off', - }, - }, - { - files: ['src/types/deprecated.ts'], - rules: { - '@typescript-eslint/naming-convention': 'off', - }, }, ], }; diff --git a/packages/feedback/package.json b/packages/feedback/package.json index bf75b64913f2..48d41f588447 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -44,7 +44,8 @@ "dependencies": { "@sentry/core": "8.0.0-alpha.2", "@sentry/types": "8.0.0-alpha.2", - "@sentry/utils": "8.0.0-alpha.2" + "@sentry/utils": "8.0.0-alpha.2", + "preact": "^10.19.4" }, "scripts": { "build": "run-p build:transpile build:types build:bundle", @@ -53,7 +54,7 @@ "build:dev": "run-p build:transpile build:types", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", - "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8", + "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8 && yarn node ./scripts/shim-preact-export.js", "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch", "build:dev:watch": "run-p build:transpile:watch build:types:watch", "build:transpile:watch": "yarn build:transpile --watch", diff --git a/packages/feedback/rollup.bundle.config.mjs b/packages/feedback/rollup.bundle.config.mjs index d35b834711c6..f5794d328409 100644 --- a/packages/feedback/rollup.bundle.config.mjs +++ b/packages/feedback/rollup.bundle.config.mjs @@ -1,12 +1,15 @@ import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; -const baseBundleConfig = makeBaseBundleConfig({ - bundleType: 'addon', - entrypoints: ['src/index.ts'], - licenseTitle: '@sentry-internal/feedback', - outputFileBase: () => 'bundles/feedback', -}); - -const builds = makeBundleConfigVariants(baseBundleConfig); - -export default builds; +export default makeBundleConfigVariants( + makeBaseBundleConfig({ + bundleType: 'addon', + entrypoints: ['src/index.ts'], + jsVersion: 'es6', + licenseTitle: '@sentry-internal/feedback', + outputFileBase: () => 'bundles/feedback', + sucrase: { + jsxPragma: 'h', + jsxFragmentPragma: 'Fragment', + }, + }), +); diff --git a/packages/feedback/rollup.npm.config.mjs b/packages/feedback/rollup.npm.config.mjs index 5a1800f23b08..3dc031c5ff82 100644 --- a/packages/feedback/rollup.npm.config.mjs +++ b/packages/feedback/rollup.npm.config.mjs @@ -12,5 +12,9 @@ export default makeNPMConfigVariants( preserveModules: false, }, }, + sucrase: { + jsxPragma: 'h', + jsxFragmentPragma: 'Fragment', + }, }), ); diff --git a/packages/feedback/scripts/shim-preact-export.js b/packages/feedback/scripts/shim-preact-export.js new file mode 100644 index 000000000000..bd74e4da0a05 --- /dev/null +++ b/packages/feedback/scripts/shim-preact-export.js @@ -0,0 +1,75 @@ +// preact does not support more modern TypeScript versions, which breaks our users that depend on older +// TypeScript versions. To fix this, we shim the types from preact to be any and remove the dependency on preact +// for types directly. This script is meant to be run after the build/npm/types-ts3.8 directory is created. + +// Path: build/npm/types-ts3.8/global.d.ts + +const fs = require('fs'); +const path = require('path'); + +/** + * This regex looks for preact imports we can replace and shim out. + * + * Example: + * import { ComponentChildren, VNode } from 'preact'; + */ +const preactImportRegex = /import\s*{\s*([\w\s,]+)\s*}\s*from\s*'preact'\s*;?/; + +function walk(dir) { + const files = fs.readdirSync(dir); + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.lstatSync(filePath); + if (stat.isDirectory()) { + walk(filePath); + } else { + if (filePath.endsWith('.d.ts')) { + const content = fs.readFileSync(filePath, 'utf8'); + const capture = preactImportRegex.exec(content); + if (capture) { + const groups = capture[1].split(',').map(s => s.trim()); + + // This generates a shim snippet to replace the type imports from preact + // It generates a snippet based on the capture groups of preactImportRegex. + // + // Example: + // + // import type { ComponentChildren, VNode } from 'preact'; + // becomes + // type ComponentChildren: any; + // type VNode: any; + const snippet = groups.reduce((acc, curr) => { + const searchableValue = curr.includes(' as ') ? curr.split(' as ')[1] : curr; + + // look to see if imported as value, then we have to use declare const + if (content.includes(`typeof ${searchableValue}`)) { + return `${acc}declare const ${searchableValue}: any;\n`; + } + + // look to see if generic type like Foo + if (content.includes(`${searchableValue}<`)) { + return `${acc}type ${searchableValue} = any;\n`; + } + + // otherwise we can just leave as type + return `${acc}type ${searchableValue} = any;\n`; + }, ''); + + // we then can remove the import from preact + const newContent = content.replace(preactImportRegex, '// replaced import from preact'); + + // and write the new content to the file + fs.writeFileSync(filePath, snippet + newContent, 'utf8'); + } + } + } + }); +} + +function run() { + // recurse through build/npm/types-ts3.8 directory + const dir = path.join('build', 'npm', 'types-ts3.8'); + walk(dir); +} + +run(); diff --git a/packages/feedback/src/constants/index.ts b/packages/feedback/src/constants/index.ts new file mode 100644 index 000000000000..9804fdedf431 --- /dev/null +++ b/packages/feedback/src/constants/index.ts @@ -0,0 +1,27 @@ +import { GLOBAL_OBJ } from '@sentry/utils'; + +export { DEFAULT_THEME } from './theme'; + +// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser` +// prevents the browser package from being bundled in the CDN bundle, and avoids a +// circular dependency between the browser and feedback packages +export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; +export const DOCUMENT = WINDOW.document; +export const NAVIGATOR = WINDOW.navigator; + +export const ACTOR_LABEL = 'Report a Bug'; +export const CANCEL_BUTTON_LABEL = 'Cancel'; +export const SUBMIT_BUTTON_LABEL = 'Send Bug Report'; +export const FORM_TITLE = 'Report a Bug'; +export const EMAIL_PLACEHOLDER = 'your.email@example.org'; +export const EMAIL_LABEL = 'Email'; +export const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?"; +export const MESSAGE_LABEL = 'Description'; +export const NAME_PLACEHOLDER = 'Your Name'; +export const NAME_LABEL = 'Name'; +export const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!'; + +export const FEEDBACK_WIDGET_SOURCE = 'widget'; +export const FEEDBACK_API_SOURCE = 'api'; + +export const SUCCESS_MESSAGE_TIMEOUT = 5000; diff --git a/packages/feedback/src/constants.ts b/packages/feedback/src/constants/theme.ts similarity index 58% rename from packages/feedback/src/constants.ts rename to packages/feedback/src/constants/theme.ts index 07782968375f..7fff31f48964 100644 --- a/packages/feedback/src/constants.ts +++ b/packages/feedback/src/constants/theme.ts @@ -1,14 +1,8 @@ -import { GLOBAL_OBJ } from '@sentry/utils'; - -// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser` -// prevents the browser package from being bundled in the CDN bundle, and avoids a -// circular dependency between the browser and feedback packages -export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; - const LIGHT_BACKGROUND = '#ffffff'; const INHERIT = 'inherit'; const SUBMIT_COLOR = 'rgba(108, 95, 199, 1)'; -const LIGHT_THEME = { + +export const LIGHT_THEME = { fontFamily: "system-ui, 'Helvetica Neue', Arial, sans-serif", fontSize: '14px', @@ -59,18 +53,3 @@ export const DEFAULT_THEME = { error: '#f55459', }, }; - -export const ACTOR_LABEL = 'Report a Bug'; -export const CANCEL_BUTTON_LABEL = 'Cancel'; -export const SUBMIT_BUTTON_LABEL = 'Send Bug Report'; -export const FORM_TITLE = 'Report a Bug'; -export const EMAIL_PLACEHOLDER = 'your.email@example.org'; -export const EMAIL_LABEL = 'Email'; -export const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?"; -export const MESSAGE_LABEL = 'Description'; -export const NAME_PLACEHOLDER = 'Your Name'; -export const NAME_LABEL = 'Name'; -export const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!'; - -export const FEEDBACK_WIDGET_SOURCE = 'widget'; -export const FEEDBACK_API_SOURCE = 'api'; diff --git a/packages/feedback/test/utils/TestClient.ts b/packages/feedback/src/core/TestClient.ts similarity index 84% rename from packages/feedback/test/utils/TestClient.ts rename to packages/feedback/src/core/TestClient.ts index 61156a3be8b0..39ed51fcff67 100644 --- a/packages/feedback/test/utils/TestClient.ts +++ b/packages/feedback/src/core/TestClient.ts @@ -4,34 +4,50 @@ import { resolvedSyncPromise } from '@sentry/utils'; export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {} +/** + * + */ export class TestClient extends BaseClient { public constructor(options: TestClientOptions) { super(options); } + /** + * + */ public eventFromException(exception: any): PromiseLike { return resolvedSyncPromise({ exception: { values: [ { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access type: exception.name, + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access value: exception.message, - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ }, ], }, }); } + /** + * + */ public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { return resolvedSyncPromise({ message, level }); } } +/** + * + */ export function init(options: TestClientOptions): void { initAndBind(TestClient, options); } +/** + * + */ export function getDefaultClientOptions(options: Partial = {}): ClientOptions { return { integrations: [], diff --git a/packages/feedback/src/widget/Actor.css.ts b/packages/feedback/src/core/components/Actor.css.ts similarity index 74% rename from packages/feedback/src/widget/Actor.css.ts rename to packages/feedback/src/core/components/Actor.css.ts index 44bd60a3418e..4e7a9466cd1e 100644 --- a/packages/feedback/src/widget/Actor.css.ts +++ b/packages/feedback/src/core/components/Actor.css.ts @@ -1,16 +1,26 @@ +import { DOCUMENT } from '../../constants'; + /** * Creates