From 7a22a83713431c1dacb4f8fd17af8119829eb8a1 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Fri, 9 Feb 2024 12:40:55 -0500
Subject: [PATCH 01/54] working feedback screenshot package with preact
---
packages/feedback/src/index.ts | 2 ++
packages/feedback/src/integration.ts | 8 ++++++++
packages/feedback/src/types/index.ts | 2 ++
packages/feedback/src/widget/Form.ts | 13 ++++++++++++-
packages/feedback/src/widget/createWidget.ts | 2 ++
5 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/packages/feedback/src/index.ts b/packages/feedback/src/index.ts
index 8fcba29c5d0f..76e9478c1ee1 100644
--- a/packages/feedback/src/index.ts
+++ b/packages/feedback/src/index.ts
@@ -4,3 +4,5 @@ export {
Feedback,
feedbackIntegration,
} from './integration';
+
+console.log('Feedback 2');
diff --git a/packages/feedback/src/integration.ts b/packages/feedback/src/integration.ts
index 501f844abeaa..f2041ea57428 100644
--- a/packages/feedback/src/integration.ts
+++ b/packages/feedback/src/integration.ts
@@ -111,6 +111,8 @@ export class Feedback implements Integration {
onFormOpen,
onSubmitError,
onSubmitSuccess,
+
+ // getScreenshotIntegration,
}: OptionalFeedbackConfiguration = {}) {
// eslint-disable-next-line deprecation/deprecation
this.name = Feedback.id;
@@ -158,6 +160,8 @@ export class Feedback implements Integration {
onFormOpen,
onSubmitError,
onSubmitSuccess,
+
+ // getScreenshotIntegration,
};
}
@@ -182,6 +186,7 @@ export class Feedback implements Integration {
this._createWidget(this.options);
} catch (err) {
DEBUG_BUILD && logger.error(err);
+ console.log(err);
}
}
@@ -241,6 +246,7 @@ export class Feedback implements Integration {
});
} catch (err) {
DEBUG_BUILD && logger.error(err);
+ console.log(err);
return null;
}
}
@@ -255,6 +261,7 @@ export class Feedback implements Integration {
return this._createWidget(mergeOptions(this.options, optionOverrides || {}));
} catch (err) {
DEBUG_BUILD && logger.error(err);
+ console.log(err);
return null;
}
}
@@ -281,6 +288,7 @@ export class Feedback implements Integration {
return true;
}
} catch (err) {
+ console.log(err);
DEBUG_BUILD && logger.error(err);
}
diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts
index c2642b54d09c..f666f2aaa8ef 100644
--- a/packages/feedback/src/types/index.ts
+++ b/packages/feedback/src/types/index.ts
@@ -80,6 +80,8 @@ export interface FeedbackGeneralConfiguration {
email: string;
name: string;
};
+
+ // getScreenshotIntegration: () => Integration;
}
/**
diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts
index 9b90cf547477..074b539ad04b 100644
--- a/packages/feedback/src/widget/Form.ts
+++ b/packages/feedback/src/widget/Form.ts
@@ -1,6 +1,7 @@
import type { FeedbackComponent, FeedbackFormData, FeedbackInternalOptions, FeedbackTextConfiguration } from '../types';
import { SubmitButton } from './SubmitButton';
import { createElement } from './util/createElement';
+import * as ScreenshotIntegration from '@sentry-internal/feedback-screenshot';
export interface FormComponentProps
extends Pick<
@@ -139,7 +140,7 @@ export function Form({
name: 'message',
required: true,
className: 'form__input form__input--textarea',
- placeholder: messagePlaceholder,
+ placeholder: 'message placeholder',
});
const cancelEl = createElement(
@@ -155,6 +156,14 @@ export function Form({
cancelButtonLabel,
);
+ const screenshot = createElement('div', { className: 'btn-group' });
+
+ // @ts-expect-error testing
+ ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
+ el: screenshot,
+ props: null,
+ });
+
const formEl = createElement(
'form',
{
@@ -219,6 +228,8 @@ export function Form({
],
),
+ screenshot,
+
createElement(
'div',
{
diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts
index b5e414803121..14558f35bbcc 100644
--- a/packages/feedback/src/widget/createWidget.ts
+++ b/packages/feedback/src/widget/createWidget.ts
@@ -76,6 +76,7 @@ export function createWidget({
} catch (err) {
// TODO: error handling
logger.error(err);
+ console.log(err);
}
}
@@ -211,6 +212,7 @@ export function createWidget({
} catch (err) {
// TODO: Error handling?
logger.error(err);
+ console.log(err);
}
}
From 4877b4f54e417f88c704f1681745d0c6fcbde5af Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Fri, 9 Feb 2024 12:41:13 -0500
Subject: [PATCH 02/54] working feedback screenshot package with preact
---
.craft.yml | 4 ++
.github/workflows/build.yml | 1 +
package.json | 1 +
packages/browser/src/index.ts | 2 +
packages/feedback-screenshot/.eslintignore | 2 +
packages/feedback-screenshot/.eslintrc.js | 25 +++++++
packages/feedback-screenshot/.gitignore | 4 ++
packages/feedback-screenshot/LICENSE | 14 ++++
packages/feedback-screenshot/README.md | 25 +++++++
packages/feedback-screenshot/jest.config.js | 6 ++
packages/feedback-screenshot/package.json | 65 +++++++++++++++++
.../rollup.bundle.config.mjs | 25 +++++++
.../feedback-screenshot/rollup.npm.config.mjs | 27 +++++++
.../scripts/craft-pre-release.sh | 8 +++
packages/feedback-screenshot/src/index.ts | 8 +++
.../feedback-screenshot/src/screenshot.ts | 52 ++++++++++++++
.../src/screenshotWidget.tsx | 12 ++++
packages/feedback-screenshot/tsconfig.json | 17 +++++
.../feedback-screenshot/tsconfig.test.json | 15 ++++
.../feedback-screenshot/tsconfig.types.json | 10 +++
yarn.lock | 70 ++++++++++++++++++-
21 files changed, 392 insertions(+), 1 deletion(-)
create mode 100644 packages/feedback-screenshot/.eslintignore
create mode 100644 packages/feedback-screenshot/.eslintrc.js
create mode 100644 packages/feedback-screenshot/.gitignore
create mode 100644 packages/feedback-screenshot/LICENSE
create mode 100644 packages/feedback-screenshot/README.md
create mode 100644 packages/feedback-screenshot/jest.config.js
create mode 100644 packages/feedback-screenshot/package.json
create mode 100644 packages/feedback-screenshot/rollup.bundle.config.mjs
create mode 100644 packages/feedback-screenshot/rollup.npm.config.mjs
create mode 100644 packages/feedback-screenshot/scripts/craft-pre-release.sh
create mode 100644 packages/feedback-screenshot/src/index.ts
create mode 100644 packages/feedback-screenshot/src/screenshot.ts
create mode 100644 packages/feedback-screenshot/src/screenshotWidget.tsx
create mode 100644 packages/feedback-screenshot/tsconfig.json
create mode 100644 packages/feedback-screenshot/tsconfig.test.json
create mode 100644 packages/feedback-screenshot/tsconfig.types.json
diff --git a/.craft.yml b/.craft.yml
index f795d317b05e..4cdb6e631364 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -36,6 +36,10 @@ targets:
- name: npm
id: '@sentry-internal/replay-canvas'
includeNames: /^sentry-internal-replay-canvas-\d.*\.tgz$/
+ ## 1.9 FeedbackScreenshot package (browser only)
+ - name: npm
+ id: '@sentry-internal/feedback-screenshot'
+ includeNames: /^sentry-internal-feedback-screenshot-\d.*\.tgz$/
## 2. Browser & Node SDKs
- name: npm
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ccda1b605e7c..c93c2cafa859 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -108,6 +108,7 @@ jobs:
- 'packages/replay/**'
- 'packages/replay-canvas/**'
- 'packages/feedback/**'
+ - 'packages/feedback-screenshot/**'
- 'packages/wasm/**'
browser_integration:
- *shared
diff --git a/package.json b/package.json
index 74fbfe2a650f..92a23fe1f217 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
"packages/eslint-config-sdk",
"packages/eslint-plugin-sdk",
"packages/feedback",
+ "packages/feedback-screenshot",
"packages/gatsby",
"packages/integrations",
"packages/integration-shims",
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 9fa4d83e3984..13c2ed777409 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -90,3 +90,5 @@ export {
BrowserProfilingIntegration,
browserProfilingIntegration,
} from './profiling/integration';
+
+console.log('browser');
diff --git a/packages/feedback-screenshot/.eslintignore b/packages/feedback-screenshot/.eslintignore
new file mode 100644
index 000000000000..b38db2f296ff
--- /dev/null
+++ b/packages/feedback-screenshot/.eslintignore
@@ -0,0 +1,2 @@
+node_modules/
+build/
diff --git a/packages/feedback-screenshot/.eslintrc.js b/packages/feedback-screenshot/.eslintrc.js
new file mode 100644
index 000000000000..eaa6b88347ca
--- /dev/null
+++ b/packages/feedback-screenshot/.eslintrc.js
@@ -0,0 +1,25 @@
+// Note: All paths are relative to the directory in which eslint is being run, rather than the directory where this file
+// lives
+
+// ESLint config docs: https://eslint.org/docs/user-guide/configuring/
+
+module.exports = {
+ extends: ['../../.eslintrc.js'],
+ overrides: [
+ {
+ files: ['src/**/*.ts'],
+ rules: {
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
+ },
+ },
+ {
+ files: ['jest.setup.ts', 'jest.config.ts'],
+ parserOptions: {
+ project: ['tsconfig.test.json'],
+ },
+ rules: {
+ 'no-console': 'off',
+ },
+ },
+ ],
+};
diff --git a/packages/feedback-screenshot/.gitignore b/packages/feedback-screenshot/.gitignore
new file mode 100644
index 000000000000..363d3467c6fa
--- /dev/null
+++ b/packages/feedback-screenshot/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/*.tgz
+.eslintcache
+build
diff --git a/packages/feedback-screenshot/LICENSE b/packages/feedback-screenshot/LICENSE
new file mode 100644
index 000000000000..ea5e82344f87
--- /dev/null
+++ b/packages/feedback-screenshot/LICENSE
@@ -0,0 +1,14 @@
+Copyright (c) 2024 Sentry (https://sentry.io) and individual contributors. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/feedback-screenshot/README.md b/packages/feedback-screenshot/README.md
new file mode 100644
index 000000000000..04caefe7ac31
--- /dev/null
+++ b/packages/feedback-screenshot/README.md
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+# Sentry Integration for Feedback with Screenshot
+
+This SDK is **considered experimental and in a beta state**. It may experience breaking changes, and may be discontinued
+at any time. Please reach out on [GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have
+any feedback/concerns.
+
+To view Feedback in Sentry, your
+[Sentry organization must be an early adopter](https://docs.sentry.io/product/accounts/early-adopter-features/).
+
+## Installation
+
+Please read the [offical integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/) for
+installation instructions.
+
+## Configuration
+
+The Feedback integration is highly customizable, please read the
+[official integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/configuration/) for the
+most up-to-date configuration options.
diff --git a/packages/feedback-screenshot/jest.config.js b/packages/feedback-screenshot/jest.config.js
new file mode 100644
index 000000000000..cd02790794a7
--- /dev/null
+++ b/packages/feedback-screenshot/jest.config.js
@@ -0,0 +1,6 @@
+const baseConfig = require('../../jest/jest.config.js');
+
+module.exports = {
+ ...baseConfig,
+ testEnvironment: 'jsdom',
+};
diff --git a/packages/feedback-screenshot/package.json b/packages/feedback-screenshot/package.json
new file mode 100644
index 000000000000..d36512257322
--- /dev/null
+++ b/packages/feedback-screenshot/package.json
@@ -0,0 +1,65 @@
+{
+ "name": "@sentry-internal/feedback-screenshot",
+ "version": "7.100.0",
+ "description": "Sentry SDK integration for user feedback screenshots",
+ "repository": "git://github.com/getsentry/sentry-javascript.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback",
+ "author": "Sentry",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "files": [
+ "cjs",
+ "esm",
+ "types",
+ "types-ts3.8"
+ ],
+ "main": "build/npm/cjs/index.js",
+ "module": "build/npm/esm/index.js",
+ "types": "build/npm/types/index.d.ts",
+ "typesVersions": {
+ "<4.9": {
+ "build/npm/types/index.d.ts": [
+ "build/npm/types-ts3.8/index.d.ts"
+ ]
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@sentry/core": "7.100.0",
+ "@sentry/types": "7.100.0",
+ "@sentry/utils": "7.100.0",
+ "preact": "^10.19.4",
+ "preact-compat": "^3.19.0",
+ "@rollup/plugin-alias": "5.1.0"
+ },
+ "scripts": {
+ "build": "run-p build:transpile build:types build:bundle",
+ "build:transpile": "rollup -c rollup.npm.config.mjs",
+ "build:bundle": "rollup -c rollup.bundle.config.mjs",
+ "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: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",
+ "build:bundle:watch": "yarn build:bundle --watch",
+ "build:types:watch": "tsc -p tsconfig.types.json --watch",
+ "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm",
+ "circularDepCheck": "madge --circular src/index.ts",
+ "clean": "rimraf build sentry-feedback-*.tgz",
+ "fix": "eslint . --format stylish --fix",
+ "lint": "eslint . --format stylish",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push --sig"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sideEffects": false
+}
diff --git a/packages/feedback-screenshot/rollup.bundle.config.mjs b/packages/feedback-screenshot/rollup.bundle.config.mjs
new file mode 100644
index 000000000000..496308b212e2
--- /dev/null
+++ b/packages/feedback-screenshot/rollup.bundle.config.mjs
@@ -0,0 +1,25 @@
+import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils';
+import alias from '@rollup/plugin-alias';
+
+const baseBundleConfig = makeBaseBundleConfig({
+ bundleType: 'addon',
+ entrypoints: ['src/index.ts'],
+ jsVersion: 'es6',
+ licenseTitle: '@sentry-internal/feedback-screenshot',
+ outputFileBase: () => 'bundles/feedback-screenshot',
+ plugins: [
+ alias({
+ entries: [
+ { find: 'react', replacement: 'preact/compat' },
+ { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
+ { find: 'react-dom', replacement: 'preact/compat' },
+ { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' }
+ ]
+ })
+ ],
+
+});
+
+const builds = makeBundleConfigVariants(baseBundleConfig);
+
+export default builds;
diff --git a/packages/feedback-screenshot/rollup.npm.config.mjs b/packages/feedback-screenshot/rollup.npm.config.mjs
new file mode 100644
index 000000000000..b9a01d8abc53
--- /dev/null
+++ b/packages/feedback-screenshot/rollup.npm.config.mjs
@@ -0,0 +1,27 @@
+import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
+import alias from '@rollup/plugin-alias';
+
+export default makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ hasBundles: true,
+ packageSpecificConfig: {
+ output: {
+ // set exports to 'named' or 'auto' so that rollup doesn't warn
+ exports: 'named',
+ // set preserveModules to false because for Replay we actually want
+ // to bundle everything into one file.
+ preserveModules: false,
+ },
+ },
+ plugins: ['@babel/transform-react-jsx', { pragma: 'h' },
+ alias({
+ entries: [
+ { find: 'react', replacement: 'preact/compat' },
+ { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
+ { find: 'react-dom', replacement: 'preact/compat' },
+ { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' }
+ ]
+ })
+ ],
+ }),
+);
diff --git a/packages/feedback-screenshot/scripts/craft-pre-release.sh b/packages/feedback-screenshot/scripts/craft-pre-release.sh
new file mode 100644
index 000000000000..bae7c3246cdb
--- /dev/null
+++ b/packages/feedback-screenshot/scripts/craft-pre-release.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -eux
+OLD_VERSION="${1}"
+NEW_VERSION="${2}"
+
+# Do not tag and commit changes made by "npm version"
+export npm_config_git_tag_version=false
+npm version "${NEW_VERSION}"
diff --git a/packages/feedback-screenshot/src/index.ts b/packages/feedback-screenshot/src/index.ts
new file mode 100644
index 000000000000..1974e8232df1
--- /dev/null
+++ b/packages/feedback-screenshot/src/index.ts
@@ -0,0 +1,8 @@
+export {
+ // eslint-disable-next-line deprecation/deprecation
+ FeedbackScreenshot,
+ feedbackScreenshotIntegration,
+} from './screenshot';
+export type { FeedbackScreenshotIntegrationOptions } from './screenshot';
+
+console.log('screenshot 6');
diff --git a/packages/feedback-screenshot/src/screenshot.ts b/packages/feedback-screenshot/src/screenshot.ts
new file mode 100644
index 000000000000..6e6873937ba2
--- /dev/null
+++ b/packages/feedback-screenshot/src/screenshot.ts
@@ -0,0 +1,52 @@
+import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
+import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
+import { Hello } from './screenshotWidget';
+import { GLOBAL_OBJ } from '@sentry/utils';
+import { h, render } from 'preact';
+
+interface FeedbackScreenshotOptions {
+ el: Element;
+ props: string;
+}
+
+export interface FeedbackScreenshotIntegrationOptions {
+ el: Element;
+ props: string;
+}
+
+const INTEGRATION_NAME = 'FeedbackScreenshot';
+const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
+
+/** Exported only for type safe tests. */
+export const _feedbackScreenshotIntegration = ((options: Partial = {}) => {
+ return {
+ name: INTEGRATION_NAME,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ setupOnce() {},
+ getOptions(): FeedbackScreenshotIntegrationOptions {
+ return { el: options.el || WINDOW.document.createElement('div'), props: options.props || 'prop' };
+ },
+ renderScreenshotWidget: (options: FeedbackScreenshotOptions) => {
+ return render(h(Hello, null), options.el);
+ },
+ };
+}) satisfies IntegrationFn;
+
+/**
+ * Add this in addition to `replayIntegration()` to enable canvas recording.
+ */
+export const feedbackScreenshotIntegration = defineIntegration(_feedbackScreenshotIntegration);
+
+/**
+ * @deprecated Use `feedbackScreenshotIntegration()` instead
+ */
+// eslint-disable-next-line deprecation/deprecation
+export const FeedbackScreenshot = convertIntegrationFnToClass(
+ INTEGRATION_NAME,
+ feedbackScreenshotIntegration,
+) as IntegrationClass<
+ Integration & {
+ getOptions: () => FeedbackScreenshotIntegrationOptions;
+ renderScreenshotWidget: () => void;
+ }
+>;
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
new file mode 100644
index 000000000000..762da67c12e9
--- /dev/null
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -0,0 +1,12 @@
+import { h, render } from 'preact';
+
+interface FeedbackScreenshotOptions {
+ el: Element;
+ props: string;
+}
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export function Hello(options: FeedbackScreenshotOptions) {
+ console.log('screenshot widget');
+ return h('div', null, 'hello');
+}
diff --git a/packages/feedback-screenshot/tsconfig.json b/packages/feedback-screenshot/tsconfig.json
new file mode 100644
index 000000000000..b71a402a0249
--- /dev/null
+++ b/packages/feedback-screenshot/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "esnext",
+
+ /* Preact Config */
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact",
+ "skipLibCheck": true,
+ "baseUrl": "./",
+ "paths": {
+ "react": ["./node_modules/preact/compat/"],
+ "react-dom": ["./node_modules/preact/compat/"]
+ }
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/feedback-screenshot/tsconfig.test.json b/packages/feedback-screenshot/tsconfig.test.json
new file mode 100644
index 000000000000..ad87caa06c48
--- /dev/null
+++ b/packages/feedback-screenshot/tsconfig.test.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["test/**/*.ts", "jest.config.ts", "jest.setup.ts"],
+
+ "compilerOptions": {
+ "types": ["node", "jest"],
+ "esModuleInterop": true,
+ "allowJs": true,
+ "noImplicitAny": true,
+ "noImplicitThis": false,
+ "strictNullChecks": true,
+ "strictPropertyInitialization": false
+ }
+}
diff --git a/packages/feedback-screenshot/tsconfig.types.json b/packages/feedback-screenshot/tsconfig.types.json
new file mode 100644
index 000000000000..374fd9bc9364
--- /dev/null
+++ b/packages/feedback-screenshot/tsconfig.types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build/npm/types"
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 9f5971f8838d..f90f4d908c76 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5293,6 +5293,13 @@
dependencies:
web-streams-polyfill "^3.1.1"
+"@rollup/plugin-alias@5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.0.tgz#99a94accc4ff9a3483be5baeedd5d7da3b597e93"
+ integrity sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==
+ dependencies:
+ slash "^4.0.0"
+
"@rollup/plugin-commonjs@24.0.0":
version "24.0.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz#fb7cf4a6029f07ec42b25daa535c75b05a43f75c"
@@ -5542,6 +5549,16 @@
semver "7.3.2"
semver-intersect "1.4.0"
+"@sentry-internal/feedback-screenshot@file:packages/feedback-screenshot":
+ version "7.100.0"
+ dependencies:
+ "@rollup/plugin-alias" "5.1.0"
+ "@sentry/core" "7.100.0"
+ "@sentry/types" "7.100.0"
+ "@sentry/utils" "7.100.0"
+ preact "^10.19.4"
+ preact-compat "^3.19.0"
+
"@sentry-internal/rrdom@2.11.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.11.0.tgz#f7c8f54705ad84ece0e97e53f12e87c687749b32"
@@ -18176,6 +18193,13 @@ image-size@~0.5.0:
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=
+immutability-helper@^2.7.1:
+ version "2.9.1"
+ resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.9.1.tgz#71c423ba387e67b6c6ceba0650572f2a2a6727df"
+ integrity sha512-r/RmRG8xO06s/k+PIaif2r5rGc3j4Yhc01jSBfwPCXDLYZwp/yxralI37Df1mwmuzcCsen/E/ITKcTEvc1PQmQ==
+ dependencies:
+ invariant "^2.2.0"
+
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
@@ -18484,7 +18508,7 @@ into-stream@^3.1.0:
from2 "^2.1.1"
p-is-promise "^1.1.0"
-invariant@^2.2.1, invariant@^2.2.2:
+invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -26239,6 +26263,40 @@ postgres-range@^1.1.1:
resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.3.tgz#9ccd7b01ca2789eb3c2e0888b3184225fa859f76"
integrity sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==
+preact-compat@^3.19.0:
+ version "3.19.0"
+ resolved "https://registry.yarnpkg.com/preact-compat/-/preact-compat-3.19.0.tgz#a71457b6a3bf051690a4411603bc2085aa061c2f"
+ integrity sha512-f83A4hIhH8Uzhb9GbIcGk8SM19ffWlwP9mDaYwQdRnMdekZwcCA7eIAbeV4EMQaV9C0Yuy8iKgBAtyTKPZQt/Q==
+ dependencies:
+ immutability-helper "^2.7.1"
+ preact-context "^1.1.3"
+ preact-render-to-string "^3.8.2"
+ preact-transition-group "^1.1.1"
+ prop-types "^15.6.2"
+ standalone-react-addons-pure-render-mixin "^0.1.1"
+
+preact-context@^1.1.3:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/preact-context/-/preact-context-1.1.4.tgz#866ebd35bef5788f73fc453f06f01b03ddf8f4ff"
+ integrity sha512-gcCjPJ65R0MiW9hDu8W/3WAmyTElIvwLyEO6oLQiM6/TbLKLxCpBCWV8GJjx52TTEyUr60HLDcmoCXZlslelzQ==
+
+preact-render-to-string@^3.8.2:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-3.8.2.tgz#bd72964d705a57da3a9e72098acaa073dd3ceff9"
+ integrity sha512-przuZPajiurStGgxMoJP0EJeC4xj5CgHv+M7GfF3YxAdhGgEWAkhOSE0xympAFN20uMayntBZpttIZqqLl77fw==
+ dependencies:
+ pretty-format "^3.5.1"
+
+preact-transition-group@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/preact-transition-group/-/preact-transition-group-1.1.1.tgz#f0a49327ea515ece34ea2be864c4a7d29e5d6e10"
+ integrity sha512-v89XLodCvylf5lMrhI+LqIhDsAjWfiDKV4keAU+L5yDtxmqn8uvjZXn+haKMEG0x0PZz81own19SuNJD5NlOFQ==
+
+preact@^10.19.4:
+ version "10.19.4"
+ resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.4.tgz#735d331d5b1bd2182cc36f2ba481fd6f0da3fe3b"
+ integrity sha512-dwaX5jAh0Ga8uENBX1hSOujmKWgx9RtL80KaKUFLc6jb4vCEAc3EeZ0rnQO/FO4VgjfPMfoLFWnNG8bHuZ9VLw==
+
prebuild-install@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
@@ -26356,6 +26414,11 @@ pretty-format@^29.7.0:
ansi-styles "^5.0.0"
react-is "^18.0.0"
+pretty-format@^3.5.1:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
+ integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
+
pretty-ms@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8"
@@ -29464,6 +29527,11 @@ stagehand@^1.0.0:
dependencies:
debug "^4.1.0"
+standalone-react-addons-pure-render-mixin@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/standalone-react-addons-pure-render-mixin/-/standalone-react-addons-pure-render-mixin-0.1.1.tgz#3c7409f4c79c40de9ac72c616cf679a994f37551"
+ integrity sha512-HFkwqpJmvz4vxHyzdeUcjtsp8Am+NauLXdU2/YXT1/InPbszaRo1cLPAy/58H7oOPNNjteqHcv04JEw+d9C+Xw==
+
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
From ef7d7e83865acc2930c8e48ec56ab2983e95ad16 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Fri, 9 Feb 2024 17:02:49 -0500
Subject: [PATCH 03/54] update rollup
---
dev-packages/rollup-utils/bundleHelpers.mjs | 6 ++++--
dev-packages/rollup-utils/npmHelpers.mjs | 3 +++
dev-packages/rollup-utils/plugins/bundlePlugins.mjs | 13 +++++++++++++
package.json | 1 +
packages/feedback-screenshot/package.json | 3 +--
.../feedback-screenshot/rollup.bundle.config.mjs | 12 ------------
packages/feedback-screenshot/rollup.npm.config.mjs | 11 -----------
packages/feedback-screenshot/src/index.ts | 2 +-
packages/feedback-screenshot/src/screenshot.ts | 6 +++---
.../feedback-screenshot/src/screenshotWidget.tsx | 11 +++--------
packages/feedback/src/widget/Form.ts | 2 +-
yarn.lock | 12 +-----------
12 files changed, 31 insertions(+), 51 deletions(-)
diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs
index 66bded3b62de..b9cd0f415ce2 100644
--- a/dev-packages/rollup-utils/bundleHelpers.mjs
+++ b/dev-packages/rollup-utils/bundleHelpers.mjs
@@ -8,6 +8,7 @@ import deepMerge from 'deepmerge';
import {
getEs5Polyfills,
+ makeAliasPlugin,
makeBrowserBuildPlugin,
makeCleanupPlugin,
makeCommonJSPlugin,
@@ -29,6 +30,7 @@ export function makeBaseBundleConfig(options) {
const isEs5 = jsVersion.toLowerCase() === 'es5';
+ const aliasPlugin = makeAliasPlugin();
const nodeResolvePlugin = makeNodeResolvePlugin();
const sucrasePlugin = makeSucrasePlugin();
const cleanupPlugin = makeCleanupPlugin();
@@ -124,8 +126,8 @@ export function makeBaseBundleConfig(options) {
esModule: false,
},
plugins: isEs5
- ? [tsPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin]
- : [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
+ ? [tsPlugin, aliasPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin]
+ : [sucrasePlugin, aliasPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
treeshake: 'smallest',
};
diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs
index 6085a502200f..8b9647065f0b 100644
--- a/dev-packages/rollup-utils/npmHelpers.mjs
+++ b/dev-packages/rollup-utils/npmHelpers.mjs
@@ -9,6 +9,7 @@ import * as path from 'path';
import deepMerge from 'deepmerge';
import {
+ makeAliasPlugin,
makeCleanupPlugin,
makeDebugBuildStatementReplacePlugin,
makeExtractPolyfillsPlugin,
@@ -30,6 +31,7 @@ export function makeBaseNPMConfig(options = {}) {
addPolyfills = true,
} = options;
+ const aliasPlugin = makeAliasPlugin();
const nodeResolvePlugin = makeNodeResolvePlugin();
const sucrasePlugin = makeSucrasePlugin({ disableESTransforms: !addPolyfills });
const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin();
@@ -91,6 +93,7 @@ export function makeBaseNPMConfig(options = {}) {
},
plugins: [
+ aliasPlugin,
nodeResolvePlugin,
setSdkSourcePlugin,
sucrasePlugin,
diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
index 1df1e2ac917d..a9c292df363b 100644
--- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
+++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
@@ -1,4 +1,5 @@
/**
+ * Alias plugin docs: https://github.com/rollup/plugins/tree/master/packages/alias
* CommonJS plugin docs: https://github.com/rollup/plugins/tree/master/packages/commonjs
* License plugin docs: https://github.com/mjeanroy/rollup-plugin-license
* Replace plugin docs: https://github.com/rollup/plugins/tree/master/packages/replace
@@ -13,6 +14,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
+import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
@@ -189,3 +191,14 @@ export function makeNodeResolvePlugin() {
}
export { commonjs as makeCommonJSPlugin };
+
+export function makeAliasPlugin() {
+ return alias({
+ entries: [
+ { find: 'react', replacement: 'preact/compat' },
+ { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
+ { find: 'react-dom', replacement: 'preact/compat' },
+ { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' }
+ ]
+ });
+}
diff --git a/package.json b/package.json
index 92a23fe1f217..0079352476fd 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
],
"devDependencies": {
"@biomejs/biome": "^1.4.0",
+ "@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
diff --git a/packages/feedback-screenshot/package.json b/packages/feedback-screenshot/package.json
index d36512257322..bba5dc2908cf 100644
--- a/packages/feedback-screenshot/package.json
+++ b/packages/feedback-screenshot/package.json
@@ -33,8 +33,7 @@
"@sentry/types": "7.100.0",
"@sentry/utils": "7.100.0",
"preact": "^10.19.4",
- "preact-compat": "^3.19.0",
- "@rollup/plugin-alias": "5.1.0"
+ "preact-compat": "^3.19.0"
},
"scripts": {
"build": "run-p build:transpile build:types build:bundle",
diff --git a/packages/feedback-screenshot/rollup.bundle.config.mjs b/packages/feedback-screenshot/rollup.bundle.config.mjs
index 496308b212e2..9766ea9c61d1 100644
--- a/packages/feedback-screenshot/rollup.bundle.config.mjs
+++ b/packages/feedback-screenshot/rollup.bundle.config.mjs
@@ -1,5 +1,4 @@
import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils';
-import alias from '@rollup/plugin-alias';
const baseBundleConfig = makeBaseBundleConfig({
bundleType: 'addon',
@@ -7,17 +6,6 @@ const baseBundleConfig = makeBaseBundleConfig({
jsVersion: 'es6',
licenseTitle: '@sentry-internal/feedback-screenshot',
outputFileBase: () => 'bundles/feedback-screenshot',
- plugins: [
- alias({
- entries: [
- { find: 'react', replacement: 'preact/compat' },
- { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
- { find: 'react-dom', replacement: 'preact/compat' },
- { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' }
- ]
- })
- ],
-
});
const builds = makeBundleConfigVariants(baseBundleConfig);
diff --git a/packages/feedback-screenshot/rollup.npm.config.mjs b/packages/feedback-screenshot/rollup.npm.config.mjs
index b9a01d8abc53..8c50a33f0afb 100644
--- a/packages/feedback-screenshot/rollup.npm.config.mjs
+++ b/packages/feedback-screenshot/rollup.npm.config.mjs
@@ -1,5 +1,4 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
-import alias from '@rollup/plugin-alias';
export default makeNPMConfigVariants(
makeBaseNPMConfig({
@@ -13,15 +12,5 @@ export default makeNPMConfigVariants(
preserveModules: false,
},
},
- plugins: ['@babel/transform-react-jsx', { pragma: 'h' },
- alias({
- entries: [
- { find: 'react', replacement: 'preact/compat' },
- { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
- { find: 'react-dom', replacement: 'preact/compat' },
- { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' }
- ]
- })
- ],
}),
);
diff --git a/packages/feedback-screenshot/src/index.ts b/packages/feedback-screenshot/src/index.ts
index 1974e8232df1..10b57b5ccc12 100644
--- a/packages/feedback-screenshot/src/index.ts
+++ b/packages/feedback-screenshot/src/index.ts
@@ -5,4 +5,4 @@ export {
} from './screenshot';
export type { FeedbackScreenshotIntegrationOptions } from './screenshot';
-console.log('screenshot 6');
+console.log('screenshot 9');
diff --git a/packages/feedback-screenshot/src/screenshot.ts b/packages/feedback-screenshot/src/screenshot.ts
index 6e6873937ba2..377728db65cd 100644
--- a/packages/feedback-screenshot/src/screenshot.ts
+++ b/packages/feedback-screenshot/src/screenshot.ts
@@ -6,12 +6,12 @@ import { h, render } from 'preact';
interface FeedbackScreenshotOptions {
el: Element;
- props: string;
+ props: unknown;
}
export interface FeedbackScreenshotIntegrationOptions {
el: Element;
- props: string;
+ props: unknown;
}
const INTEGRATION_NAME = 'FeedbackScreenshot';
@@ -24,7 +24,7 @@ export const _feedbackScreenshotIntegration = ((options: Partial {
return render(h(Hello, null), options.el);
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index 762da67c12e9..6d89121ff7d4 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -1,12 +1,7 @@
-import { h, render } from 'preact';
-
-interface FeedbackScreenshotOptions {
- el: Element;
- props: string;
-}
+import { h, createElement } from 'preact';
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function Hello(options: FeedbackScreenshotOptions) {
- console.log('screenshot widget');
+export function Hello() {
+ console.log('screenshot widget 1');
return h('div', null, 'hello');
}
diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts
index 074b539ad04b..353f923be64e 100644
--- a/packages/feedback/src/widget/Form.ts
+++ b/packages/feedback/src/widget/Form.ts
@@ -158,7 +158,7 @@ export function Form({
const screenshot = createElement('div', { className: 'btn-group' });
- // @ts-expect-error testing
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
el: screenshot,
props: null,
diff --git a/yarn.lock b/yarn.lock
index f90f4d908c76..842634a522f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5293,7 +5293,7 @@
dependencies:
web-streams-polyfill "^3.1.1"
-"@rollup/plugin-alias@5.1.0":
+"@rollup/plugin-alias@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.0.tgz#99a94accc4ff9a3483be5baeedd5d7da3b597e93"
integrity sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==
@@ -5549,16 +5549,6 @@
semver "7.3.2"
semver-intersect "1.4.0"
-"@sentry-internal/feedback-screenshot@file:packages/feedback-screenshot":
- version "7.100.0"
- dependencies:
- "@rollup/plugin-alias" "5.1.0"
- "@sentry/core" "7.100.0"
- "@sentry/types" "7.100.0"
- "@sentry/utils" "7.100.0"
- preact "^10.19.4"
- preact-compat "^3.19.0"
-
"@sentry-internal/rrdom@2.11.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.11.0.tgz#f7c8f54705ad84ece0e97e53f12e87c687749b32"
From 0395331e211cc23f52fd5b4a6e386d38f0edc96d Mon Sep 17 00:00:00 2001
From: Billy Vong
Date: Fri, 9 Feb 2024 21:48:36 -0500
Subject: [PATCH 04/54] allow passing options to sucrase + pass jsxPragma
---
dev-packages/rollup-utils/bundleHelpers.mjs | 2 +-
dev-packages/rollup-utils/npmHelpers.mjs | 3 ++-
packages/feedback-screenshot/rollup.bundle.config.mjs | 4 ++++
packages/feedback-screenshot/rollup.npm.config.mjs | 4 ++++
4 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs
index b9cd0f415ce2..a111ef0e6d46 100644
--- a/dev-packages/rollup-utils/bundleHelpers.mjs
+++ b/dev-packages/rollup-utils/bundleHelpers.mjs
@@ -32,7 +32,7 @@ export function makeBaseBundleConfig(options) {
const aliasPlugin = makeAliasPlugin();
const nodeResolvePlugin = makeNodeResolvePlugin();
- const sucrasePlugin = makeSucrasePlugin();
+ const sucrasePlugin = makeSucrasePlugin(options.sucrase);
const cleanupPlugin = makeCleanupPlugin();
const markAsBrowserBuildPlugin = makeBrowserBuildPlugin(true);
const licensePlugin = makeLicensePlugin(licenseTitle);
diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs
index 8b9647065f0b..12ddd7e331a0 100644
--- a/dev-packages/rollup-utils/npmHelpers.mjs
+++ b/dev-packages/rollup-utils/npmHelpers.mjs
@@ -29,11 +29,12 @@ export function makeBaseNPMConfig(options = {}) {
hasBundles = false,
packageSpecificConfig = {},
addPolyfills = true,
+ sucrase = {},
} = options;
const aliasPlugin = makeAliasPlugin();
const nodeResolvePlugin = makeNodeResolvePlugin();
- const sucrasePlugin = makeSucrasePlugin({ disableESTransforms: !addPolyfills });
+ const sucrasePlugin = makeSucrasePlugin({ disableESTransforms: !addPolyfills, ...sucrase });
const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin();
const cleanupPlugin = makeCleanupPlugin();
const extractPolyfillsPlugin = makeExtractPolyfillsPlugin();
diff --git a/packages/feedback-screenshot/rollup.bundle.config.mjs b/packages/feedback-screenshot/rollup.bundle.config.mjs
index 9766ea9c61d1..6bae4e9c57c9 100644
--- a/packages/feedback-screenshot/rollup.bundle.config.mjs
+++ b/packages/feedback-screenshot/rollup.bundle.config.mjs
@@ -6,6 +6,10 @@ const baseBundleConfig = makeBaseBundleConfig({
jsVersion: 'es6',
licenseTitle: '@sentry-internal/feedback-screenshot',
outputFileBase: () => 'bundles/feedback-screenshot',
+ sucrase: {
+ jsxPragma: 'h',
+ jsxFragmentPragma: 'Fragment',
+ }
});
const builds = makeBundleConfigVariants(baseBundleConfig);
diff --git a/packages/feedback-screenshot/rollup.npm.config.mjs b/packages/feedback-screenshot/rollup.npm.config.mjs
index 8c50a33f0afb..b659c384eff5 100644
--- a/packages/feedback-screenshot/rollup.npm.config.mjs
+++ b/packages/feedback-screenshot/rollup.npm.config.mjs
@@ -12,5 +12,9 @@ export default makeNPMConfigVariants(
preserveModules: false,
},
},
+ sucrase: {
+ jsxPragma: 'h',
+ jsxFragmentPragma: 'Fragment',
+ }
}),
);
From 364e18794e6103daf011ef19bce29e15557979e4 Mon Sep 17 00:00:00 2001
From: Billy Vong
Date: Fri, 9 Feb 2024 21:48:57 -0500
Subject: [PATCH 05/54] proper ts-expect-error
---
packages/feedback/src/widget/Form.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts
index 353f923be64e..a0ebad037276 100644
--- a/packages/feedback/src/widget/Form.ts
+++ b/packages/feedback/src/widget/Form.ts
@@ -158,7 +158,7 @@ export function Form({
const screenshot = createElement('div', { className: 'btn-group' });
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ // @ts-expect-error temp
ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
el: screenshot,
props: null,
From 510f37cd287cb410078c2a52be44b4a6f6d51f07 Mon Sep 17 00:00:00 2001
From: Billy Vong
Date: Fri, 9 Feb 2024 21:49:20 -0500
Subject: [PATCH 06/54] fix for preact
---
packages/feedback-screenshot/src/screenshotWidget.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index 6d89121ff7d4..bd1471968c29 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -1,7 +1,6 @@
-import { h, createElement } from 'preact';
+import {h} from 'preact';
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function Hello() {
- console.log('screenshot widget 1');
- return h('div', null, 'hello');
+ return Hello 2
;
}
From 513ab3e58edef31fcbbcc7695a471ba02c1ec5df Mon Sep 17 00:00:00 2001
From: Billy Vong
Date: Fri, 9 Feb 2024 21:49:28 -0500
Subject: [PATCH 07/54] change to tsx and fix for preact
---
.../feedback-screenshot/src/{screenshot.ts => screenshot.tsx} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename packages/feedback-screenshot/src/{screenshot.ts => screenshot.tsx} (97%)
diff --git a/packages/feedback-screenshot/src/screenshot.ts b/packages/feedback-screenshot/src/screenshot.tsx
similarity index 97%
rename from packages/feedback-screenshot/src/screenshot.ts
rename to packages/feedback-screenshot/src/screenshot.tsx
index 377728db65cd..def1904457c7 100644
--- a/packages/feedback-screenshot/src/screenshot.ts
+++ b/packages/feedback-screenshot/src/screenshot.tsx
@@ -27,7 +27,7 @@ export const _feedbackScreenshotIntegration = ((options: Partial {
- return render(h(Hello, null), options.el);
+ return render(, options.el);
},
};
}) satisfies IntegrationFn;
From 7d9b800d991d2ee42124012d10d5e7096bf12aa2 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Mon, 12 Feb 2024 13:08:49 -0500
Subject: [PATCH 08/54] screenshot button
---
packages/feedback-screenshot/src/screenshot.tsx | 4 ++--
.../feedback-screenshot/src/screenshotWidget.tsx | 13 ++++++++++---
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/packages/feedback-screenshot/src/screenshot.tsx b/packages/feedback-screenshot/src/screenshot.tsx
index def1904457c7..fd87d10cdb96 100644
--- a/packages/feedback-screenshot/src/screenshot.tsx
+++ b/packages/feedback-screenshot/src/screenshot.tsx
@@ -1,6 +1,6 @@
import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
-import { Hello } from './screenshotWidget';
+import { ScreenshotButton } from './screenshotWidget';
import { GLOBAL_OBJ } from '@sentry/utils';
import { h, render } from 'preact';
@@ -27,7 +27,7 @@ export const _feedbackScreenshotIntegration = ((options: Partial {
- return render(, options.el);
+ return render(, options.el);
},
};
}) satisfies IntegrationFn;
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index bd1471968c29..0d2aa7b3c575 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -1,6 +1,13 @@
-import {h} from 'preact';
+import { h } from 'preact';
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function Hello() {
- return Hello 2
;
+export function ScreenshotButton() {
+ return (
+
+ );
}
From 8dbd500baec8021c8006d49ebeeb41989a6c3c45 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Mon, 12 Feb 2024 16:21:23 -0500
Subject: [PATCH 09/54] screenshot button and widget placeholders
---
packages/feedback-screenshot/src/index.ts | 2 +-
.../feedback-screenshot/src/screenshot.tsx | 7 ++++++-
.../src/screenshotButton.tsx | 18 ++++++++++++++++++
.../src/screenshotWidget.tsx | 12 ++----------
packages/feedback/src/index.ts | 2 +-
packages/feedback/src/widget/Dialog.ts | 10 ++++++++++
packages/feedback/src/widget/Form.ts | 10 +++++-----
7 files changed, 43 insertions(+), 18 deletions(-)
create mode 100644 packages/feedback-screenshot/src/screenshotButton.tsx
diff --git a/packages/feedback-screenshot/src/index.ts b/packages/feedback-screenshot/src/index.ts
index 10b57b5ccc12..ddbdaca3bbde 100644
--- a/packages/feedback-screenshot/src/index.ts
+++ b/packages/feedback-screenshot/src/index.ts
@@ -5,4 +5,4 @@ export {
} from './screenshot';
export type { FeedbackScreenshotIntegrationOptions } from './screenshot';
-console.log('screenshot 9');
+console.log('screenshot 11');
diff --git a/packages/feedback-screenshot/src/screenshot.tsx b/packages/feedback-screenshot/src/screenshot.tsx
index fd87d10cdb96..e8618899d9fb 100644
--- a/packages/feedback-screenshot/src/screenshot.tsx
+++ b/packages/feedback-screenshot/src/screenshot.tsx
@@ -1,6 +1,7 @@
import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
-import { ScreenshotButton } from './screenshotWidget';
+import { ScreenshotButton } from './screenshotButton';
+import { ScreenshotWidget } from './screenshotWidget';
import { GLOBAL_OBJ } from '@sentry/utils';
import { h, render } from 'preact';
@@ -27,6 +28,9 @@ export const _feedbackScreenshotIntegration = ((options: Partial {
+ return render(, options.el);
+ },
+ renderScreenshotButton: (options: FeedbackScreenshotOptions) => {
return render(, options.el);
},
};
@@ -48,5 +52,6 @@ export const FeedbackScreenshot = convertIntegrationFnToClass(
Integration & {
getOptions: () => FeedbackScreenshotIntegrationOptions;
renderScreenshotWidget: () => void;
+ renderScreenshotButton: () => void;
}
>;
diff --git a/packages/feedback-screenshot/src/screenshotButton.tsx b/packages/feedback-screenshot/src/screenshotButton.tsx
new file mode 100644
index 000000000000..3811f6a93a85
--- /dev/null
+++ b/packages/feedback-screenshot/src/screenshotButton.tsx
@@ -0,0 +1,18 @@
+import { Component, h } from 'preact';
+
+export class ScreenshotButton extends Component {
+ state = { clicked: false };
+ handleClick = () => {
+ this.setState({ clicked: !this.state.clicked });
+ };
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index 0d2aa7b3c575..84461099ad00 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -1,13 +1,5 @@
import { h } from 'preact';
-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function ScreenshotButton() {
- return (
-
- );
+export function ScreenshotWidget() {
+ return ;
}
diff --git a/packages/feedback/src/index.ts b/packages/feedback/src/index.ts
index 76e9478c1ee1..dbb24a745cfe 100644
--- a/packages/feedback/src/index.ts
+++ b/packages/feedback/src/index.ts
@@ -5,4 +5,4 @@ export {
feedbackIntegration,
} from './integration';
-console.log('Feedback 2');
+console.log('Feedback 4');
diff --git a/packages/feedback/src/widget/Dialog.ts b/packages/feedback/src/widget/Dialog.ts
index 84b4318b34d1..bdc009970080 100644
--- a/packages/feedback/src/widget/Dialog.ts
+++ b/packages/feedback/src/widget/Dialog.ts
@@ -3,6 +3,7 @@ import type { FormComponentProps } from './Form';
import { Form } from './Form';
import { Logo } from './Logo';
import { createElement } from './util/createElement';
+import * as ScreenshotIntegration from '@sentry-internal/feedback-screenshot';
export interface DialogProps
extends FormComponentProps,
@@ -95,6 +96,14 @@ export function Dialog({
return (el && el.open === true) || false;
}
+ const screenshot = createElement('div', { className: 'dialog-content' });
+
+ // @ts-expect-error temp
+ ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
+ el: screenshot,
+ props: null,
+ });
+
const {
el: formEl,
showError,
@@ -119,6 +128,7 @@ export function Dialog({
open: true,
onClick: handleDialogClick,
},
+ screenshot,
createElement(
'div',
{
diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts
index a0ebad037276..77da1b5eaf90 100644
--- a/packages/feedback/src/widget/Form.ts
+++ b/packages/feedback/src/widget/Form.ts
@@ -140,7 +140,7 @@ export function Form({
name: 'message',
required: true,
className: 'form__input form__input--textarea',
- placeholder: 'message placeholder',
+ placeholder: messagePlaceholder,
});
const cancelEl = createElement(
@@ -156,11 +156,11 @@ export function Form({
cancelButtonLabel,
);
- const screenshot = createElement('div', { className: 'btn-group' });
+ const button = createElement('div', { className: 'btn-group' });
// @ts-expect-error temp
- ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
- el: screenshot,
+ ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotButton({
+ el: button,
props: null,
});
@@ -228,7 +228,7 @@ export function Form({
],
),
- screenshot,
+ button,
createElement(
'div',
From 4a007a10ae23c42898795dfe526bafdf05d76ed4 Mon Sep 17 00:00:00 2001
From: Ryan Albrecht
Date: Tue, 13 Feb 2024 15:59:47 -0800
Subject: [PATCH 10/54] Make feedback depend on feedback-screenshot for now,
until we have aync loading
---
packages/feedback/package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/feedback/package.json b/packages/feedback/package.json
index 060defb0510c..e7ee5e8ea626 100644
--- a/packages/feedback/package.json
+++ b/packages/feedback/package.json
@@ -29,6 +29,7 @@
"access": "public"
},
"dependencies": {
+ "@sentry-internal/feedback-screenshot": "7.100.0",
"@sentry/core": "7.100.0",
"@sentry/types": "7.100.0",
"@sentry/utils": "7.100.0"
From 0de030fc4c8499f301ef38fd8f622383fbea421a Mon Sep 17 00:00:00 2001
From: Ryan Albrecht
Date: Tue, 13 Feb 2024 16:02:29 -0800
Subject: [PATCH 11/54] Convert screenshotButton to a functional component, and
add types to components
---
.../src/screenshotButton.tsx | 33 ++++++++++---------
.../src/screenshotWidget.tsx | 7 ++--
2 files changed, 21 insertions(+), 19 deletions(-)
diff --git a/packages/feedback-screenshot/src/screenshotButton.tsx b/packages/feedback-screenshot/src/screenshotButton.tsx
index 3811f6a93a85..e4787d07664d 100644
--- a/packages/feedback-screenshot/src/screenshotButton.tsx
+++ b/packages/feedback-screenshot/src/screenshotButton.tsx
@@ -1,18 +1,19 @@
-import { Component, h } from 'preact';
+import {h, render} from 'preact';
+import {useState, useCallback} from 'preact/hooks';
+import type {VNode} from 'preact';
-export class ScreenshotButton extends Component {
- state = { clicked: false };
- handleClick = () => {
- this.setState({ clicked: !this.state.clicked });
- };
- render() {
- return (
-
- );
- }
+export function ScreenshotButton(): VNode {
+ const [clicked, setClicked] = useState(false);
+ const handleClick = useCallback(() => {
+ setClicked(prev => !prev);
+ }, []);
+
+ return (
+
+ );
}
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index 84461099ad00..4062e37e42a8 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -1,5 +1,6 @@
-import { h } from 'preact';
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function ScreenshotWidget() {
+import {h, render} from 'preact';
+import type {VNode} from 'preact';
+
+export function ScreenshotWidget(): VNode {
return ;
}
From 8ea52d321a89c24cf88692e9c75fac67b4c7a4cc Mon Sep 17 00:00:00 2001
From: Ryan Albrecht
Date: Tue, 13 Feb 2024 16:17:23 -0800
Subject: [PATCH 12/54] Update docblock for feedbackScreenshotIntegration
---
packages/feedback-screenshot/src/screenshot.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/feedback-screenshot/src/screenshot.tsx b/packages/feedback-screenshot/src/screenshot.tsx
index e8618899d9fb..e539b1422763 100644
--- a/packages/feedback-screenshot/src/screenshot.tsx
+++ b/packages/feedback-screenshot/src/screenshot.tsx
@@ -37,7 +37,7 @@ export const _feedbackScreenshotIntegration = ((options: Partial
Date: Thu, 15 Feb 2024 15:05:58 -0500
Subject: [PATCH 13/54] take screenshot
---
.../feedback-screenshot/src/screenshot.tsx | 18 ++++---
.../src/screenshotButton.tsx | 26 ++++++---
.../src/screenshotWidget.tsx | 14 +++--
.../src/useTakeScreenshot.tsx | 54 +++++++++++++++++++
4 files changed, 93 insertions(+), 19 deletions(-)
create mode 100644 packages/feedback-screenshot/src/useTakeScreenshot.tsx
diff --git a/packages/feedback-screenshot/src/screenshot.tsx b/packages/feedback-screenshot/src/screenshot.tsx
index e539b1422763..adbe5f6eb67e 100644
--- a/packages/feedback-screenshot/src/screenshot.tsx
+++ b/packages/feedback-screenshot/src/screenshot.tsx
@@ -1,17 +1,18 @@
import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
import { ScreenshotButton } from './screenshotButton';
-import { ScreenshotWidget } from './screenshotWidget';
import { GLOBAL_OBJ } from '@sentry/utils';
import { h, render } from 'preact';
interface FeedbackScreenshotOptions {
- el: Element;
+ buttonRef: HTMLDivElement;
+ croppingRef: HTMLDivElement;
props: unknown;
}
export interface FeedbackScreenshotIntegrationOptions {
- el: Element;
+ buttonRef: HTMLDivElement;
+ croppingRef: HTMLDivElement;
props: unknown;
}
@@ -25,13 +26,14 @@ export const _feedbackScreenshotIntegration = ((options: Partial {
- return render(, options.el);
- },
- renderScreenshotButton: (options: FeedbackScreenshotOptions) => {
- return render(, options.el);
+ return render(, options.buttonRef);
},
};
}) satisfies IntegrationFn;
diff --git a/packages/feedback-screenshot/src/screenshotButton.tsx b/packages/feedback-screenshot/src/screenshotButton.tsx
index e4787d07664d..87506f57877d 100644
--- a/packages/feedback-screenshot/src/screenshotButton.tsx
+++ b/packages/feedback-screenshot/src/screenshotButton.tsx
@@ -1,17 +1,29 @@
-import {h, render} from 'preact';
-import {useState, useCallback} from 'preact/hooks';
-import type {VNode} from 'preact';
+import { h, render } from 'preact';
+import { useState, useCallback, useEffect } from 'preact/hooks';
+import { useTakeScreenshot } from './useTakeScreenshot';
+import type { VNode } from 'preact';
+import { ScreenshotWidget } from './screenshotWidget';
-export function ScreenshotButton(): VNode {
+type Props = { croppingRef: HTMLDivElement };
+
+export function ScreenshotButton({ croppingRef }: Props): VNode {
const [clicked, setClicked] = useState(false);
- const handleClick = useCallback(() => {
+ const { isInProgress, takeScreenshot } = useTakeScreenshot();
+
+ const handleClick = useCallback(async () => {
+ if (!clicked) {
+ const image = await takeScreenshot();
+ render(, croppingRef);
+ } else {
+ render(null, croppingRef);
+ }
setClicked(prev => !prev);
- }, []);
+ }, [clicked]);
return (
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index 4062e37e42a8..5ed692ddf13f 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -1,6 +1,12 @@
-import {h, render} from 'preact';
-import type {VNode} from 'preact';
+import { h, render } from 'preact';
+import type { VNode } from 'preact';
-export function ScreenshotWidget(): VNode {
- return ;
+type Props = { image: string | undefined };
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export function ScreenshotWidget({ image }: Props): VNode | null {
+ return image ? (
+
+

+
+ ) : null;
}
diff --git a/packages/feedback-screenshot/src/useTakeScreenshot.tsx b/packages/feedback-screenshot/src/useTakeScreenshot.tsx
new file mode 100644
index 000000000000..7cda51d8ebe6
--- /dev/null
+++ b/packages/feedback-screenshot/src/useTakeScreenshot.tsx
@@ -0,0 +1,54 @@
+import { useCallback, useState } from 'preact/hooks';
+import { h } from 'preact';
+
+const takeScreenshot = async (): Promise => {
+ const stream = await navigator.mediaDevices.getDisplayMedia({
+ video: {
+ width: window.innerWidth * window.devicePixelRatio,
+ height: window.innerHeight * window.devicePixelRatio,
+ },
+ audio: false,
+ preferCurrentTab: true,
+ surfaceSwitching: 'exclude',
+ } as any);
+ const videoTrack = stream.getVideoTracks()[0];
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ if (!context) {
+ throw new Error('Could not get canvas context');
+ }
+ const video = document.createElement('video');
+ video.srcObject = new MediaStream([videoTrack]);
+
+ await new Promise(resolve => {
+ video.onloadedmetadata = () => {
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+ context.drawImage(video, 0, 0);
+ stream.getTracks().forEach(track => track.stop());
+ resolve();
+ };
+ video.play();
+ });
+
+ return canvas.toDataURL();
+};
+
+export const useTakeScreenshot = () => {
+ const [isInProgress, setIsInProgress] = useState(false);
+
+ const takeScreenshotCallback = useCallback(async (): Promise => {
+ setIsInProgress(true);
+ let image: string | null = null;
+ try {
+ image = await takeScreenshot();
+ } catch (error) {
+ setIsInProgress(false);
+ throw error;
+ }
+ setIsInProgress(false);
+ return image;
+ }, []);
+
+ return { isInProgress, takeScreenshot: takeScreenshotCallback };
+};
From f9b6f31d991404d7ccf5ddeb43c86af51ce55a41 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Thu, 15 Feb 2024 15:06:32 -0500
Subject: [PATCH 14/54] take screenshot
---
packages/feedback/src/widget/Dialog.ts | 14 ++++++++------
packages/feedback/src/widget/Form.ts | 13 +++++--------
2 files changed, 13 insertions(+), 14 deletions(-)
diff --git a/packages/feedback/src/widget/Dialog.ts b/packages/feedback/src/widget/Dialog.ts
index bdc009970080..aa663006c580 100644
--- a/packages/feedback/src/widget/Dialog.ts
+++ b/packages/feedback/src/widget/Dialog.ts
@@ -98,16 +98,11 @@ export function Dialog({
const screenshot = createElement('div', { className: 'dialog-content' });
- // @ts-expect-error temp
- ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
- el: screenshot,
- props: null,
- });
-
const {
el: formEl,
showError,
hideError,
+ screenshotButton,
} = Form({
showEmail,
showName,
@@ -121,6 +116,13 @@ export function Dialog({
...textLabels,
});
+ // @ts-expect-error temp
+ ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
+ croppingRef: screenshot,
+ buttonRef: screenshotButton,
+ props: null,
+ });
+
el = createElement(
'dialog',
{
diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts
index 77da1b5eaf90..09d553681ebe 100644
--- a/packages/feedback/src/widget/Form.ts
+++ b/packages/feedback/src/widget/Form.ts
@@ -34,6 +34,8 @@ interface FormComponent extends FeedbackComponent {
* Hides the error message
*/
hideError: () => void;
+
+ screenshotButton: HTMLDivElement;
}
function retrieveStringValue(formData: FormData, key: string): string {
@@ -156,13 +158,7 @@ export function Form({
cancelButtonLabel,
);
- const button = createElement('div', { className: 'btn-group' });
-
- // @ts-expect-error temp
- ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotButton({
- el: button,
- props: null,
- });
+ const screenshotButton = createElement('div', { className: 'btn-group' });
const formEl = createElement(
'form',
@@ -228,7 +224,7 @@ export function Form({
],
),
- button,
+ screenshotButton,
createElement(
'div',
@@ -244,6 +240,7 @@ export function Form({
get el() {
return formEl;
},
+ screenshotButton,
showError,
hideError,
};
From 354bb5239efeafd75e6a1529299dea2126c6e205 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Fri, 16 Feb 2024 17:11:21 -0500
Subject: [PATCH 15/54] move screenshot into form
---
.../src/screenshotWidget.tsx | 12 +-
.../src/useTakeScreenshot.tsx | 8 +-
packages/feedback/src/types/index.ts | 1 +
packages/feedback/src/widget/Dialog.css.ts | 17 ++-
packages/feedback/src/widget/Dialog.ts | 12 --
packages/feedback/src/widget/Form.ts | 131 ++++++++++--------
packages/feedback/src/widget/createWidget.ts | 10 ++
7 files changed, 111 insertions(+), 80 deletions(-)
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index 5ed692ddf13f..e8e58f7a7bd2 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -1,12 +1,18 @@
import { h, render } from 'preact';
import type { VNode } from 'preact';
-type Props = { image: string | undefined };
+type Props = { image: HTMLCanvasElement };
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function ScreenshotWidget({ image }: Props): VNode | null {
return image ? (
-
-

+
+
) : null;
}
diff --git a/packages/feedback-screenshot/src/useTakeScreenshot.tsx b/packages/feedback-screenshot/src/useTakeScreenshot.tsx
index 7cda51d8ebe6..e031931ef598 100644
--- a/packages/feedback-screenshot/src/useTakeScreenshot.tsx
+++ b/packages/feedback-screenshot/src/useTakeScreenshot.tsx
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'preact/hooks';
import { h } from 'preact';
-const takeScreenshot = async (): Promise
=> {
+const takeScreenshot = async (): Promise => {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
width: window.innerWidth * window.devicePixelRatio,
@@ -31,15 +31,15 @@ const takeScreenshot = async (): Promise => {
video.play();
});
- return canvas.toDataURL();
+ return canvas;
};
export const useTakeScreenshot = () => {
const [isInProgress, setIsInProgress] = useState(false);
- const takeScreenshotCallback = useCallback(async (): Promise => {
+ const takeScreenshotCallback = useCallback(async (): Promise => {
setIsInProgress(true);
- let image: string | null = null;
+ let image: HTMLCanvasElement | null = null;
try {
image = await takeScreenshot();
} catch (error) {
diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts
index f666f2aaa8ef..07d083400815 100644
--- a/packages/feedback/src/types/index.ts
+++ b/packages/feedback/src/types/index.ts
@@ -30,6 +30,7 @@ export interface FeedbackFormData {
message: string;
email?: string;
name?: string;
+ screenshot?: Uint8Array;
}
/**
diff --git a/packages/feedback/src/widget/Dialog.css.ts b/packages/feedback/src/widget/Dialog.css.ts
index b3924f00524a..f37242e77b39 100644
--- a/packages/feedback/src/widget/Dialog.css.ts
+++ b/packages/feedback/src/widget/Dialog.css.ts
@@ -42,8 +42,7 @@ export function createDialogStyles(d: Document): HTMLStyleElement {
background-color: var(--background);
color: var(--foreground);
- width: 320px;
- max-width: 100%;
+ max-width: calc(100% - 2rem);
max-height: calc(100% - 2rem);
display: flex;
flex-direction: column;
@@ -73,11 +72,21 @@ export function createDialogStyles(d: Document): HTMLStyleElement {
}
.form {
- display: grid;
+ display: flex;
overflow: auto;
+ flex-direction: row;
+ padding: 0 24px 24px;
+}
+
+.screenshot {
+ border-color: var(--submit-border);
+}
+
+.info {
+ display: flex;
flex-direction: column;
gap: 16px;
- padding: 0 24px 24px;
+ width: 272px;
}
.form__error-container {
diff --git a/packages/feedback/src/widget/Dialog.ts b/packages/feedback/src/widget/Dialog.ts
index aa663006c580..84b4318b34d1 100644
--- a/packages/feedback/src/widget/Dialog.ts
+++ b/packages/feedback/src/widget/Dialog.ts
@@ -3,7 +3,6 @@ import type { FormComponentProps } from './Form';
import { Form } from './Form';
import { Logo } from './Logo';
import { createElement } from './util/createElement';
-import * as ScreenshotIntegration from '@sentry-internal/feedback-screenshot';
export interface DialogProps
extends FormComponentProps,
@@ -96,13 +95,10 @@ export function Dialog({
return (el && el.open === true) || false;
}
- const screenshot = createElement('div', { className: 'dialog-content' });
-
const {
el: formEl,
showError,
hideError,
- screenshotButton,
} = Form({
showEmail,
showName,
@@ -116,13 +112,6 @@ export function Dialog({
...textLabels,
});
- // @ts-expect-error temp
- ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
- croppingRef: screenshot,
- buttonRef: screenshotButton,
- props: null,
- });
-
el = createElement(
'dialog',
{
@@ -130,7 +119,6 @@ export function Dialog({
open: true,
onClick: handleDialogClick,
},
- screenshot,
createElement(
'div',
{
diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts
index 09d553681ebe..463017f24125 100644
--- a/packages/feedback/src/widget/Form.ts
+++ b/packages/feedback/src/widget/Form.ts
@@ -34,8 +34,6 @@ interface FormComponent extends FeedbackComponent {
* Hides the error message
*/
hideError: () => void;
-
- screenshotButton: HTMLDivElement;
}
function retrieveStringValue(formData: FormData, key: string): string {
@@ -83,10 +81,12 @@ export function Form({
try {
if (onSubmit) {
const formData = new FormData(e.target as HTMLFormElement);
+ const imageData = undefined;
const feedback = {
name: retrieveStringValue(formData, 'name'),
email: retrieveStringValue(formData, 'email'),
message: retrieveStringValue(formData, 'message'),
+ screenshot: imageData,
};
onSubmit(feedback);
@@ -158,8 +158,17 @@ export function Form({
cancelButtonLabel,
);
+ const screenshot = createElement('div', { className: 'screenshot' });
+
const screenshotButton = createElement('div', { className: 'btn-group' });
+ // @ts-expect-error temp
+ ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
+ croppingRef: screenshot,
+ buttonRef: screenshotButton,
+ props: null,
+ });
+
const formEl = createElement(
'form',
{
@@ -168,70 +177,79 @@ export function Form({
},
[
errorEl,
+ screenshot,
- showName &&
- createElement(
- 'label',
- {
- htmlFor: 'name',
- className: 'form__label',
- },
- [
+ createElement(
+ 'div',
+ {
+ className: 'info',
+ },
+ [
+ showName &&
createElement(
- 'span',
- { className: 'form__label__text' },
- nameLabel,
- isNameRequired && createElement('span', { className: 'form__label__text--required' }, ' (required)'),
+ 'label',
+ {
+ htmlFor: 'name',
+ className: 'form__label',
+ },
+ [
+ createElement(
+ 'span',
+ { className: 'form__label__text' },
+ nameLabel,
+ isNameRequired && createElement('span', { className: 'form__label__text--required' }, ' (required)'),
+ ),
+ nameEl,
+ ],
),
- nameEl,
- ],
- ),
- !showName && nameEl,
-
- showEmail &&
- createElement(
- 'label',
- {
- htmlFor: 'email',
- className: 'form__label',
- },
- [
+ !showName && nameEl,
+
+ showEmail &&
createElement(
- 'span',
- { className: 'form__label__text' },
- emailLabel,
- isEmailRequired && createElement('span', { className: 'form__label__text--required' }, ' (required)'),
+ 'label',
+ {
+ htmlFor: 'email',
+ className: 'form__label',
+ },
+ [
+ createElement(
+ 'span',
+ { className: 'form__label__text' },
+ emailLabel,
+ isEmailRequired && createElement('span', { className: 'form__label__text--required' }, ' (required)'),
+ ),
+ emailEl,
+ ],
),
- emailEl,
- ],
- ),
- !showEmail && emailEl,
+ !showEmail && emailEl,
- createElement(
- 'label',
- {
- htmlFor: 'message',
- className: 'form__label',
- },
- [
createElement(
- 'span',
- { className: 'form__label__text' },
- messageLabel,
- createElement('span', { className: 'form__label__text--required' }, ' (required)'),
+ 'label',
+ {
+ htmlFor: 'message',
+ className: 'form__label',
+ },
+ [
+ createElement(
+ 'span',
+ { className: 'form__label__text' },
+ messageLabel,
+ createElement('span', { className: 'form__label__text--required' }, ' (required)'),
+ ),
+ messageEl,
+ ],
),
- messageEl,
- ],
- ),
- screenshotButton,
+ screenshotButton,
- createElement(
- 'div',
- {
- className: 'btn-group',
- },
- [submitEl, cancelEl],
+ createElement(
+ 'div',
+ {
+ className: 'btn-group',
+ },
+ [submitEl, cancelEl],
+ ),
+ ],
),
],
);
@@ -240,7 +258,6 @@ export function Form({
get el() {
return formEl;
},
- screenshotButton,
showError,
hideError,
};
diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts
index 14558f35bbcc..505602e6a04e 100644
--- a/packages/feedback/src/widget/createWidget.ts
+++ b/packages/feedback/src/widget/createWidget.ts
@@ -1,5 +1,6 @@
import { getCurrentScope } from '@sentry/core';
import { logger } from '@sentry/utils';
+import * as Sentry from '@sentry/browser';
import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget } from '../types';
import { handleFeedbackSubmit } from '../util/handleFeedbackSubmit';
@@ -119,6 +120,15 @@ export function createWidget({
// Success
removeDialog();
showSuccessMessage();
+ Sentry.withScope(scope => {
+ if (feedback.screenshot) {
+ scope.addAttachment({
+ filename: 'screenshot.png',
+ data: feedback.screenshot,
+ contentType: 'image/png',
+ });
+ }
+ });
if (options.onSubmitSuccess) {
options.onSubmitSuccess();
From 22efe6ea099edc73c357f2e4aeb391cd663ddb77 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Thu, 22 Feb 2024 00:53:08 -0500
Subject: [PATCH 16/54] working screenshot attachment
---
packages/core/src/envelope.ts | 30 ++++++++++++
packages/core/src/index.ts | 9 +---
.../feedback-screenshot/src/screenshot.tsx | 26 ++++++++--
.../src/screenshotButton.tsx | 12 +++--
.../src/screenshotWidget.tsx | 8 +++-
packages/feedback/src/sendFeedback.ts | 4 +-
packages/feedback/src/types/index.ts | 8 +++-
.../feedback/src/util/handleFeedbackSubmit.ts | 5 +-
.../feedback/src/util/sendFeedbackRequest.ts | 45 +++++++++++++++++-
packages/feedback/src/widget/Form.ts | 47 ++++++++++++++++---
packages/feedback/src/widget/createWidget.ts | 17 ++-----
11 files changed, 167 insertions(+), 44 deletions(-)
diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts
index 9ec29c9d2a7e..02a58a2af31e 100644
--- a/packages/core/src/envelope.ts
+++ b/packages/core/src/envelope.ts
@@ -1,4 +1,6 @@
import type {
+ Attachment,
+ AttachmentItem,
DsnComponents,
Event,
EventEnvelope,
@@ -9,8 +11,10 @@ import type {
SessionAggregates,
SessionEnvelope,
SessionItem,
+ TextEncoderInternal,
} from '@sentry/types';
import {
+ createAttachmentEnvelopeItem,
createEnvelope,
createEventEnvelopeHeaders,
dsnToString,
@@ -86,3 +90,29 @@ export function createEventEnvelope(
const eventItem: EventItem = [{ type: eventType }, event];
return createEnvelope(envelopeHeaders, [eventItem]);
}
+
+/**
+ * Create an Envelope from an event.
+ */
+export function createAttachmentEnvelope(
+ event: Event,
+ attachment: Attachment,
+ dsn?: DsnComponents,
+ metadata?: SdkMetadata,
+ tunnel?: string,
+ textEncoder?: TextEncoderInternal,
+): 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 attachmentItem: AttachmentItem = createAttachmentEnvelopeItem(attachment, textEncoder);
+ return createEnvelope(envelopeHeaders, [attachmentItem]);
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 849e34f6c92b..330e16120121 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -7,7 +7,7 @@ export type { IntegrationIndex } from './integration';
export * from './tracing';
export * from './semanticAttributes';
-export { createEventEnvelope, createSessionEnvelope } from './envelope';
+export { createEventEnvelope, createSessionEnvelope, createAttachmentEnvelope } from './envelope';
export {
addBreadcrumb,
captureCheckIn,
@@ -83,12 +83,7 @@ export { hasTracingEnabled } from './utils/hasTracingEnabled';
export { isSentryRequestUrl } from './utils/isSentryRequestUrl';
export { handleCallbackErrors } from './utils/handleCallbackErrors';
export { parameterize } from './utils/parameterize';
-export {
- spanToTraceHeader,
- spanToJSON,
- spanIsSampled,
- spanToTraceContext,
-} from './utils/spanUtils';
+export { spanToTraceHeader, spanToJSON, spanIsSampled, spanToTraceContext } from './utils/spanUtils';
export { getRootSpan } from './utils/getRootSpan';
export { applySdkMetadata } from './utils/sdkMetadata';
export { DEFAULT_ENVIRONMENT } from './constants';
diff --git a/packages/feedback-screenshot/src/screenshot.tsx b/packages/feedback-screenshot/src/screenshot.tsx
index adbe5f6eb67e..3cdd91986de9 100644
--- a/packages/feedback-screenshot/src/screenshot.tsx
+++ b/packages/feedback-screenshot/src/screenshot.tsx
@@ -7,20 +7,26 @@ import { h, render } from 'preact';
interface FeedbackScreenshotOptions {
buttonRef: HTMLDivElement;
croppingRef: HTMLDivElement;
- props: unknown;
+ props: {
+ screenshotImage: HTMLCanvasElement | null;
+ setScreenshotImage: (screenshot: HTMLCanvasElement | null) => void;
+ };
}
export interface FeedbackScreenshotIntegrationOptions {
buttonRef: HTMLDivElement;
croppingRef: HTMLDivElement;
- props: unknown;
+ props: {
+ screenshotImage: HTMLCanvasElement | null;
+ setScreenshotImage: (screenshot: HTMLCanvasElement | null) => void;
+ };
}
const INTEGRATION_NAME = 'FeedbackScreenshot';
const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
/** Exported only for type safe tests. */
-export const _feedbackScreenshotIntegration = ((options: Partial = {}) => {
+export const _feedbackScreenshotIntegration = ((options: FeedbackScreenshotOptions) => {
return {
name: INTEGRATION_NAME,
// eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -29,11 +35,21 @@ export const _feedbackScreenshotIntegration = ((options: Partial {
- return render(, options.buttonRef);
+ return render(
+ ,
+ options.buttonRef,
+ );
},
};
}) satisfies IntegrationFn;
diff --git a/packages/feedback-screenshot/src/screenshotButton.tsx b/packages/feedback-screenshot/src/screenshotButton.tsx
index 87506f57877d..a2e853f2567d 100644
--- a/packages/feedback-screenshot/src/screenshotButton.tsx
+++ b/packages/feedback-screenshot/src/screenshotButton.tsx
@@ -4,17 +4,23 @@ import { useTakeScreenshot } from './useTakeScreenshot';
import type { VNode } from 'preact';
import { ScreenshotWidget } from './screenshotWidget';
-type Props = { croppingRef: HTMLDivElement };
+type Props = {
+ croppingRef: HTMLDivElement;
+ screenshotImage: HTMLCanvasElement | null;
+ setScreenshotImage: (screenshot: HTMLCanvasElement | null) => void;
+};
-export function ScreenshotButton({ croppingRef }: Props): VNode {
+export function ScreenshotButton({ croppingRef, screenshotImage, setScreenshotImage }: Props): VNode {
const [clicked, setClicked] = useState(false);
const { isInProgress, takeScreenshot } = useTakeScreenshot();
const handleClick = useCallback(async () => {
if (!clicked) {
const image = await takeScreenshot();
- render(, croppingRef);
+ setScreenshotImage(image);
+ render(, croppingRef);
} else {
+ setScreenshotImage(null);
render(null, croppingRef);
}
setClicked(prev => !prev);
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index e8e58f7a7bd2..3c64c86db85a 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -1,9 +1,13 @@
import { h, render } from 'preact';
import type { VNode } from 'preact';
-type Props = { image: HTMLCanvasElement };
+type Props = {
+ screenshotImage: HTMLCanvasElement | null;
+ setScreenshotImage: (screenshot: HTMLCanvasElement | null) => void;
+};
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function ScreenshotWidget({ image }: Props): VNode | null {
+export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props): VNode | null {
+ const image = screenshotImage;
return image ? (
![]()
{
if (!message) {
throw new Error('Unable to submit feedback with empty message');
@@ -34,5 +35,6 @@ export function sendFeedback(
},
},
options,
+ screenshots,
);
}
diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts
index 07d083400815..f0c74af5dfa5 100644
--- a/packages/feedback/src/types/index.ts
+++ b/packages/feedback/src/types/index.ts
@@ -21,6 +21,7 @@ export interface SendFeedbackOptions {
* Should include replay with the feedback?
*/
includeReplay?: boolean;
+ screenshots?: Screenshot[];
}
/**
@@ -30,7 +31,6 @@ export interface FeedbackFormData {
message: string;
email?: string;
name?: string;
- screenshot?: Uint8Array;
}
/**
@@ -343,3 +343,9 @@ export interface FeedbackWidget {
closeDialog: () => void;
removeDialog: () => void;
}
+
+export interface Screenshot {
+ filename: string;
+ data: Uint8Array;
+ contentType: string;
+}
diff --git a/packages/feedback/src/util/handleFeedbackSubmit.ts b/packages/feedback/src/util/handleFeedbackSubmit.ts
index abb3aeb1368d..0d6a15cc0496 100644
--- a/packages/feedback/src/util/handleFeedbackSubmit.ts
+++ b/packages/feedback/src/util/handleFeedbackSubmit.ts
@@ -4,7 +4,7 @@ import { logger } from '@sentry/utils';
import { FEEDBACK_WIDGET_SOURCE } from '../constants';
import { DEBUG_BUILD } from '../debug-build';
import { sendFeedback } from '../sendFeedback';
-import type { FeedbackFormData, SendFeedbackOptions } from '../types';
+import type { FeedbackFormData, Screenshot, SendFeedbackOptions } from '../types';
import type { DialogComponent } from '../widget/Dialog';
/**
@@ -14,6 +14,7 @@ import type { DialogComponent } from '../widget/Dialog';
export async function handleFeedbackSubmit(
dialog: DialogComponent | null,
feedback: FeedbackFormData,
+ screenshots?: Screenshot[],
options?: SendFeedbackOptions,
): Promise
{
if (!dialog) {
@@ -31,7 +32,7 @@ export async function handleFeedbackSubmit(
dialog.hideError();
try {
- const resp = await sendFeedback({ ...feedback, source: FEEDBACK_WIDGET_SOURCE }, options);
+ const resp = await sendFeedback({ ...feedback, source: FEEDBACK_WIDGET_SOURCE }, options, screenshots);
// Success!
return resp;
diff --git a/packages/feedback/src/util/sendFeedbackRequest.ts b/packages/feedback/src/util/sendFeedbackRequest.ts
index f1629a00670a..a515fba48652 100644
--- a/packages/feedback/src/util/sendFeedbackRequest.ts
+++ b/packages/feedback/src/util/sendFeedbackRequest.ts
@@ -1,8 +1,8 @@
-import { createEventEnvelope, getClient, withScope } from '@sentry/core';
+import { createEventEnvelope, getClient, withScope, createAttachmentEnvelope } from '@sentry/core';
import type { FeedbackEvent, TransportMakeRequestResponse } from '@sentry/types';
import { FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE } from '../constants';
-import type { SendFeedbackData, SendFeedbackOptions } from '../types';
+import type { Screenshot, SendFeedbackData, SendFeedbackOptions } from '../types';
import { prepareFeedbackEvent } from './prepareFeedbackEvent';
/**
@@ -11,6 +11,7 @@ import { prepareFeedbackEvent } from './prepareFeedbackEvent';
export async function sendFeedbackRequest(
{ feedback: { message, email, name, source, url } }: SendFeedbackData,
{ includeReplay = true }: SendFeedbackOptions = {},
+ screenshots: Screenshot[],
): Promise {
const client = getClient();
const transport = client && client.getTransport();
@@ -57,6 +58,19 @@ export async function sendFeedbackRequest(
const envelope = createEventEnvelope(feedbackEvent, dsn, client.getOptions()._metadata, client.getOptions().tunnel);
+ let attachment_envelope;
+ for (const attachment of screenshots || []) {
+ attachment_envelope = createAttachmentEnvelope(
+ feedbackEvent,
+ attachment,
+ dsn,
+ client.getOptions()._metadata,
+ client.getOptions().tunnel,
+ // eslint-disable-next-line @sentry-internal/sdk/no-optional-chaining
+ client.getOptions().transportOptions && client.getOptions().transportOptions?.textEncoder,
+ );
+ }
+
let response: void | TransportMakeRequestResponse;
try {
@@ -84,6 +98,33 @@ export async function sendFeedbackRequest(
throw new Error('Unable to send Feedback');
}
+ if (attachment_envelope) {
+ try {
+ response = await transport.send(attachment_envelope);
+ } catch (err) {
+ const error = new Error('Unable to send Feedback screenshot');
+
+ 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;
+ }
+
+ // 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 screenshot');
+ }
+ }
+
return response;
});
}
diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts
index 463017f24125..18e287556f85 100644
--- a/packages/feedback/src/widget/Form.ts
+++ b/packages/feedback/src/widget/Form.ts
@@ -1,4 +1,10 @@
-import type { FeedbackComponent, FeedbackFormData, FeedbackInternalOptions, FeedbackTextConfiguration } from '../types';
+import type {
+ FeedbackComponent,
+ FeedbackFormData,
+ FeedbackInternalOptions,
+ FeedbackTextConfiguration,
+ Screenshot,
+} from '../types';
import { SubmitButton } from './SubmitButton';
import { createElement } from './util/createElement';
import * as ScreenshotIntegration from '@sentry-internal/feedback-screenshot';
@@ -21,7 +27,7 @@ export interface FormComponentProps
*/
defaultEmail: string;
onCancel?: (e: Event) => void;
- onSubmit?: (feedback: FeedbackFormData) => void;
+ onSubmit?: (feedback: FeedbackFormData, screenshots?: Screenshot[]) => void;
}
interface FormComponent extends FeedbackComponent {
@@ -44,6 +50,22 @@ function retrieveStringValue(formData: FormData, key: string): string {
return '';
}
+async function canvasToUint8Array(image: HTMLCanvasElement): Promise {
+ const canvasToBlob = (canvas: HTMLCanvasElement): Promise => {
+ return new Promise(resolve => {
+ canvas.toBlob(blob => {
+ resolve(blob);
+ });
+ });
+ };
+ const blob = await canvasToBlob(image);
+ if (blob) {
+ const blobData = await blob.arrayBuffer();
+ return new Uint8Array(blobData);
+ }
+ return null;
+}
+
/**
* Creates the form element
*/
@@ -67,11 +89,15 @@ export function Form({
onCancel,
onSubmit,
}: FormComponentProps): FormComponent {
+ let screenshotImage: HTMLCanvasElement | null = null;
+ function setScreenshotImage(newScreenshot: HTMLCanvasElement | null): void {
+ screenshotImage = newScreenshot;
+ }
const { el: submitEl } = SubmitButton({
label: submitButtonLabel,
});
- function handleSubmit(e: Event): void {
+ async function handleSubmit(e: Event): Promise {
e.preventDefault();
if (!(e.target instanceof HTMLFormElement)) {
@@ -81,15 +107,22 @@ export function Form({
try {
if (onSubmit) {
const formData = new FormData(e.target as HTMLFormElement);
- const imageData = undefined;
const feedback = {
name: retrieveStringValue(formData, 'name'),
email: retrieveStringValue(formData, 'email'),
message: retrieveStringValue(formData, 'message'),
- screenshot: imageData,
};
- onSubmit(feedback);
+ if (screenshotImage) {
+ const screenshotBlob = await canvasToUint8Array(screenshotImage);
+ if (screenshotBlob) {
+ onSubmit(feedback, [
+ { filename: 'screenshot.png', data: screenshotBlob, contentType: 'application/octet-stream' },
+ ]);
+ }
+ } else {
+ onSubmit(feedback);
+ }
}
} catch {
// pass
@@ -166,7 +199,7 @@ export function Form({
ScreenshotIntegration.feedbackScreenshotIntegration().renderScreenshotWidget({
croppingRef: screenshot,
buttonRef: screenshotButton,
- props: null,
+ props: { screenshotImage, setScreenshotImage },
});
const formEl = createElement(
diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts
index 505602e6a04e..87f6f464294c 100644
--- a/packages/feedback/src/widget/createWidget.ts
+++ b/packages/feedback/src/widget/createWidget.ts
@@ -1,8 +1,7 @@
import { getCurrentScope } from '@sentry/core';
import { logger } from '@sentry/utils';
-import * as Sentry from '@sentry/browser';
-import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget } from '../types';
+import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget, Screenshot } from '../types';
import { handleFeedbackSubmit } from '../util/handleFeedbackSubmit';
import type { ActorComponent } from './Actor';
import { Actor } from './Actor';
@@ -77,7 +76,6 @@ export function createWidget({
} catch (err) {
// TODO: error handling
logger.error(err);
- console.log(err);
}
}
@@ -85,7 +83,7 @@ export function createWidget({
* Handler for when the feedback form is completed by the user. This will
* create and send the feedback message as an event.
*/
- async function _handleFeedbackSubmit(feedback: FeedbackFormData): Promise {
+ async function _handleFeedbackSubmit(feedback: FeedbackFormData, screenshots?: Screenshot[]): Promise {
if (!dialog) {
return;
}
@@ -106,7 +104,7 @@ export function createWidget({
return;
}
- const result = await handleFeedbackSubmit(dialog, feedback);
+ const result = await handleFeedbackSubmit(dialog, feedback, screenshots);
// Error submitting feedback
if (!result) {
@@ -120,15 +118,6 @@ export function createWidget({
// Success
removeDialog();
showSuccessMessage();
- Sentry.withScope(scope => {
- if (feedback.screenshot) {
- scope.addAttachment({
- filename: 'screenshot.png',
- data: feedback.screenshot,
- contentType: 'image/png',
- });
- }
- });
if (options.onSubmitSuccess) {
options.onSubmitSuccess();
From ea7b909f5a54174f66124aa4a5ae0d680473d51b Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Thu, 22 Feb 2024 10:52:55 -0500
Subject: [PATCH 17/54] clean up attachment code
---
packages/core/src/envelope.ts | 11 +++----
packages/feedback/src/sendFeedback.ts | 5 ++--
packages/feedback/src/types/index.ts | 10 ++-----
.../feedback/src/util/handleFeedbackSubmit.ts | 6 ++--
.../feedback/src/util/sendFeedbackRequest.ts | 29 ++++++++-----------
packages/feedback/src/widget/Form.ts | 11 ++-----
packages/feedback/src/widget/createWidget.ts | 8 ++---
7 files changed, 32 insertions(+), 48 deletions(-)
diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts
index 02a58a2af31e..3189cdc5278a 100644
--- a/packages/core/src/envelope.ts
+++ b/packages/core/src/envelope.ts
@@ -11,7 +11,6 @@ import type {
SessionAggregates,
SessionEnvelope,
SessionItem,
- TextEncoderInternal,
} from '@sentry/types';
import {
createAttachmentEnvelopeItem,
@@ -96,11 +95,10 @@ export function createEventEnvelope(
*/
export function createAttachmentEnvelope(
event: Event,
- attachment: Attachment,
+ attachments: Attachment[],
dsn?: DsnComponents,
metadata?: SdkMetadata,
tunnel?: string,
- textEncoder?: TextEncoderInternal,
): EventEnvelope {
const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata);
enhanceEventWithSdkInfo(event, metadata && metadata.sdk);
@@ -113,6 +111,9 @@ export function createAttachmentEnvelope(
// of this `delete`, lest we miss putting it back in the next time the property is in use.)
delete event.sdkProcessingMetadata;
- const attachmentItem: AttachmentItem = createAttachmentEnvelopeItem(attachment, textEncoder);
- return createEnvelope(envelopeHeaders, [attachmentItem]);
+ const attachmentItems: AttachmentItem[] = [];
+ for (const attachment of attachments || []) {
+ attachmentItems.push(createAttachmentEnvelopeItem(attachment));
+ }
+ return createEnvelope(envelopeHeaders, attachmentItems);
}
diff --git a/packages/feedback/src/sendFeedback.ts b/packages/feedback/src/sendFeedback.ts
index f786c16f5548..956f901189cb 100644
--- a/packages/feedback/src/sendFeedback.ts
+++ b/packages/feedback/src/sendFeedback.ts
@@ -1,8 +1,9 @@
import { getLocationHref } from '@sentry/utils';
import { FEEDBACK_API_SOURCE } from './constants';
-import type { Screenshot, SendFeedbackOptions } from './types';
+import type { SendFeedbackOptions } from './types';
import { sendFeedbackRequest } from './util/sendFeedbackRequest';
+import type { Attachment } from '@sentry/types';
interface SendFeedbackParams {
message: string;
@@ -18,7 +19,7 @@ interface SendFeedbackParams {
export function sendFeedback(
{ name, email, message, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams,
options: SendFeedbackOptions = {},
- screenshots: Screenshot[] = [],
+ screenshots: Attachment[] = [],
): ReturnType {
if (!message) {
throw new Error('Unable to submit feedback with empty message');
diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts
index f0c74af5dfa5..adb9c4bf341d 100644
--- a/packages/feedback/src/types/index.ts
+++ b/packages/feedback/src/types/index.ts
@@ -1,4 +1,4 @@
-import type { Primitive } from '@sentry/types';
+import type { Attachment, Primitive } from '@sentry/types';
import type { ActorComponent } from '../widget/Actor';
import type { DialogComponent } from '../widget/Dialog';
@@ -21,7 +21,7 @@ export interface SendFeedbackOptions {
* Should include replay with the feedback?
*/
includeReplay?: boolean;
- screenshots?: Screenshot[];
+ screenshots?: Attachment[];
}
/**
@@ -343,9 +343,3 @@ export interface FeedbackWidget {
closeDialog: () => void;
removeDialog: () => void;
}
-
-export interface Screenshot {
- filename: string;
- data: Uint8Array;
- contentType: string;
-}
diff --git a/packages/feedback/src/util/handleFeedbackSubmit.ts b/packages/feedback/src/util/handleFeedbackSubmit.ts
index 0d6a15cc0496..1143413843f9 100644
--- a/packages/feedback/src/util/handleFeedbackSubmit.ts
+++ b/packages/feedback/src/util/handleFeedbackSubmit.ts
@@ -1,10 +1,10 @@
-import type { TransportMakeRequestResponse } from '@sentry/types';
+import type { Attachment, TransportMakeRequestResponse } from '@sentry/types';
import { logger } from '@sentry/utils';
import { FEEDBACK_WIDGET_SOURCE } from '../constants';
import { DEBUG_BUILD } from '../debug-build';
import { sendFeedback } from '../sendFeedback';
-import type { FeedbackFormData, Screenshot, SendFeedbackOptions } from '../types';
+import type { FeedbackFormData, SendFeedbackOptions } from '../types';
import type { DialogComponent } from '../widget/Dialog';
/**
@@ -14,7 +14,7 @@ import type { DialogComponent } from '../widget/Dialog';
export async function handleFeedbackSubmit(
dialog: DialogComponent | null,
feedback: FeedbackFormData,
- screenshots?: Screenshot[],
+ screenshots?: Attachment[],
options?: SendFeedbackOptions,
): Promise {
if (!dialog) {
diff --git a/packages/feedback/src/util/sendFeedbackRequest.ts b/packages/feedback/src/util/sendFeedbackRequest.ts
index 76527c4ddd0c..8a1a6129aedd 100644
--- a/packages/feedback/src/util/sendFeedbackRequest.ts
+++ b/packages/feedback/src/util/sendFeedbackRequest.ts
@@ -1,8 +1,8 @@
import { createEventEnvelope, getClient, withScope, createAttachmentEnvelope } from '@sentry/core';
-import type { FeedbackEvent, TransportMakeRequestResponse } from '@sentry/types';
+import type { Attachment, FeedbackEvent, TransportMakeRequestResponse } from '@sentry/types';
import { FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE } from '../constants';
-import type { Screenshot, SendFeedbackData, SendFeedbackOptions } from '../types';
+import type { SendFeedbackData, SendFeedbackOptions } from '../types';
import { prepareFeedbackEvent } from './prepareFeedbackEvent';
/**
@@ -11,7 +11,7 @@ import { prepareFeedbackEvent } from './prepareFeedbackEvent';
export async function sendFeedbackRequest(
{ feedback: { message, email, name, source, url } }: SendFeedbackData,
{ includeReplay = true }: SendFeedbackOptions = {},
- screenshots: Screenshot[],
+ screenshots: Attachment[],
): Promise {
const client = getClient();
const transport = client && client.getTransport();
@@ -56,19 +56,6 @@ export async function sendFeedbackRequest(
const envelope = createEventEnvelope(feedbackEvent, dsn, client.getOptions()._metadata, client.getOptions().tunnel);
- let attachment_envelope;
- for (const attachment of screenshots || []) {
- attachment_envelope = createAttachmentEnvelope(
- feedbackEvent,
- attachment,
- dsn,
- client.getOptions()._metadata,
- client.getOptions().tunnel,
- // eslint-disable-next-line @sentry-internal/sdk/no-optional-chaining
- client.getOptions().transportOptions && client.getOptions().transportOptions?.textEncoder,
- );
- }
-
let response: void | TransportMakeRequestResponse;
try {
@@ -96,7 +83,15 @@ export async function sendFeedbackRequest(
throw new Error('Unable to send Feedback');
}
- if (attachment_envelope) {
+ if (screenshots) {
+ const attachment_envelope = createAttachmentEnvelope(
+ feedbackEvent,
+ screenshots,
+ dsn,
+ client.getOptions()._metadata,
+ client.getOptions().tunnel,
+ );
+
try {
response = await transport.send(attachment_envelope);
} catch (err) {
diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts
index 18e287556f85..37793f57df9e 100644
--- a/packages/feedback/src/widget/Form.ts
+++ b/packages/feedback/src/widget/Form.ts
@@ -1,10 +1,5 @@
-import type {
- FeedbackComponent,
- FeedbackFormData,
- FeedbackInternalOptions,
- FeedbackTextConfiguration,
- Screenshot,
-} from '../types';
+import type { Attachment } from '@sentry/types';
+import type { FeedbackComponent, FeedbackFormData, FeedbackInternalOptions, FeedbackTextConfiguration } from '../types';
import { SubmitButton } from './SubmitButton';
import { createElement } from './util/createElement';
import * as ScreenshotIntegration from '@sentry-internal/feedback-screenshot';
@@ -27,7 +22,7 @@ export interface FormComponentProps
*/
defaultEmail: string;
onCancel?: (e: Event) => void;
- onSubmit?: (feedback: FeedbackFormData, screenshots?: Screenshot[]) => void;
+ onSubmit?: (feedback: FeedbackFormData, screenshots?: Attachment[]) => void;
}
interface FormComponent extends FeedbackComponent {
diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts
index 87f6f464294c..41064c48450f 100644
--- a/packages/feedback/src/widget/createWidget.ts
+++ b/packages/feedback/src/widget/createWidget.ts
@@ -1,7 +1,7 @@
import { getCurrentScope } from '@sentry/core';
import { logger } from '@sentry/utils';
-
-import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget, Screenshot } from '../types';
+import type { Attachment } from '@sentry/types';
+import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget } from '../types';
import { handleFeedbackSubmit } from '../util/handleFeedbackSubmit';
import type { ActorComponent } from './Actor';
import { Actor } from './Actor';
@@ -83,7 +83,7 @@ export function createWidget({
* Handler for when the feedback form is completed by the user. This will
* create and send the feedback message as an event.
*/
- async function _handleFeedbackSubmit(feedback: FeedbackFormData, screenshots?: Screenshot[]): Promise {
+ async function _handleFeedbackSubmit(feedback: FeedbackFormData, screenshots?: Attachment[]): Promise {
if (!dialog) {
return;
}
@@ -210,8 +210,6 @@ export function createWidget({
}
} catch (err) {
// TODO: Error handling?
- logger.error(err);
- console.log(err);
}
}
From 1b216b976ade27b987adc9374159ad40fd4a7ff2 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Thu, 22 Feb 2024 12:15:44 -0500
Subject: [PATCH 18/54] remove alias plugin
---
dev-packages/rollup-utils/bundleHelpers.mjs | 6 ++----
dev-packages/rollup-utils/npmHelpers.mjs | 3 ---
dev-packages/rollup-utils/plugins/bundlePlugins.mjs | 13 -------------
package.json | 1 -
yarn.lock | 7 -------
5 files changed, 2 insertions(+), 28 deletions(-)
diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs
index a111ef0e6d46..609a4057e2fe 100644
--- a/dev-packages/rollup-utils/bundleHelpers.mjs
+++ b/dev-packages/rollup-utils/bundleHelpers.mjs
@@ -8,7 +8,6 @@ import deepMerge from 'deepmerge';
import {
getEs5Polyfills,
- makeAliasPlugin,
makeBrowserBuildPlugin,
makeCleanupPlugin,
makeCommonJSPlugin,
@@ -30,7 +29,6 @@ export function makeBaseBundleConfig(options) {
const isEs5 = jsVersion.toLowerCase() === 'es5';
- const aliasPlugin = makeAliasPlugin();
const nodeResolvePlugin = makeNodeResolvePlugin();
const sucrasePlugin = makeSucrasePlugin(options.sucrase);
const cleanupPlugin = makeCleanupPlugin();
@@ -126,8 +124,8 @@ export function makeBaseBundleConfig(options) {
esModule: false,
},
plugins: isEs5
- ? [tsPlugin, aliasPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin]
- : [sucrasePlugin, aliasPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
+ ? [tsPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin]
+ : [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
treeshake: 'smallest',
};
diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs
index 12ddd7e331a0..18f753043ac7 100644
--- a/dev-packages/rollup-utils/npmHelpers.mjs
+++ b/dev-packages/rollup-utils/npmHelpers.mjs
@@ -9,7 +9,6 @@ import * as path from 'path';
import deepMerge from 'deepmerge';
import {
- makeAliasPlugin,
makeCleanupPlugin,
makeDebugBuildStatementReplacePlugin,
makeExtractPolyfillsPlugin,
@@ -32,7 +31,6 @@ export function makeBaseNPMConfig(options = {}) {
sucrase = {},
} = options;
- const aliasPlugin = makeAliasPlugin();
const nodeResolvePlugin = makeNodeResolvePlugin();
const sucrasePlugin = makeSucrasePlugin({ disableESTransforms: !addPolyfills, ...sucrase });
const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin();
@@ -94,7 +92,6 @@ export function makeBaseNPMConfig(options = {}) {
},
plugins: [
- aliasPlugin,
nodeResolvePlugin,
setSdkSourcePlugin,
sucrasePlugin,
diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
index a9c292df363b..1df1e2ac917d 100644
--- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
+++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
@@ -1,5 +1,4 @@
/**
- * Alias plugin docs: https://github.com/rollup/plugins/tree/master/packages/alias
* CommonJS plugin docs: https://github.com/rollup/plugins/tree/master/packages/commonjs
* License plugin docs: https://github.com/mjeanroy/rollup-plugin-license
* Replace plugin docs: https://github.com/rollup/plugins/tree/master/packages/replace
@@ -14,7 +13,6 @@ import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
-import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
@@ -191,14 +189,3 @@ export function makeNodeResolvePlugin() {
}
export { commonjs as makeCommonJSPlugin };
-
-export function makeAliasPlugin() {
- return alias({
- entries: [
- { find: 'react', replacement: 'preact/compat' },
- { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
- { find: 'react-dom', replacement: 'preact/compat' },
- { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' }
- ]
- });
-}
diff --git a/package.json b/package.json
index 0ed2b04e43d8..ef36567d47e3 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,6 @@
],
"devDependencies": {
"@biomejs/biome": "^1.4.0",
- "@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
diff --git a/yarn.lock b/yarn.lock
index 9a1418d369c9..884c47a0187b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5344,13 +5344,6 @@
dependencies:
web-streams-polyfill "^3.1.1"
-"@rollup/plugin-alias@^5.1.0":
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.0.tgz#99a94accc4ff9a3483be5baeedd5d7da3b597e93"
- integrity sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==
- dependencies:
- slash "^4.0.0"
-
"@rollup/plugin-commonjs@24.0.0":
version "24.0.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz#fb7cf4a6029f07ec42b25daa535c75b05a43f75c"
From b03f61a0ad3a2c93da665d0ae3d283b4b60df892 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Thu, 22 Feb 2024 16:15:56 -0500
Subject: [PATCH 19/54] remove dialog when screenshotting
---
packages/feedback-screenshot/src/screenshotButton.tsx | 2 +-
.../src/{screenshotWidget.tsx => screenshotEditor.tsx} | 0
packages/feedback-screenshot/src/useTakeScreenshot.tsx | 5 +++++
3 files changed, 6 insertions(+), 1 deletion(-)
rename packages/feedback-screenshot/src/{screenshotWidget.tsx => screenshotEditor.tsx} (100%)
diff --git a/packages/feedback-screenshot/src/screenshotButton.tsx b/packages/feedback-screenshot/src/screenshotButton.tsx
index a2e853f2567d..4b72e80eac51 100644
--- a/packages/feedback-screenshot/src/screenshotButton.tsx
+++ b/packages/feedback-screenshot/src/screenshotButton.tsx
@@ -2,7 +2,7 @@ import { h, render } from 'preact';
import { useState, useCallback, useEffect } from 'preact/hooks';
import { useTakeScreenshot } from './useTakeScreenshot';
import type { VNode } from 'preact';
-import { ScreenshotWidget } from './screenshotWidget';
+import { ScreenshotWidget } from './screenshotEditor';
type Props = {
croppingRef: HTMLDivElement;
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotEditor.tsx
similarity index 100%
rename from packages/feedback-screenshot/src/screenshotWidget.tsx
rename to packages/feedback-screenshot/src/screenshotEditor.tsx
diff --git a/packages/feedback-screenshot/src/useTakeScreenshot.tsx b/packages/feedback-screenshot/src/useTakeScreenshot.tsx
index e031931ef598..09fffc535a9e 100644
--- a/packages/feedback-screenshot/src/useTakeScreenshot.tsx
+++ b/packages/feedback-screenshot/src/useTakeScreenshot.tsx
@@ -40,10 +40,15 @@ export const useTakeScreenshot = () => {
const takeScreenshotCallback = useCallback(async (): Promise => {
setIsInProgress(true);
let image: HTMLCanvasElement | null = null;
+ const style = document.createElement('style');
+ style.innerHTML = '.dialog { display: none; }';
+ document.getElementById('sentry-feedback')?.shadowRoot?.appendChild(style);
try {
image = await takeScreenshot();
+ document.getElementById('sentry-feedback')?.shadowRoot?.removeChild(style);
} catch (error) {
setIsInProgress(false);
+ document.getElementById('sentry-feedback')?.shadowRoot?.removeChild(style);
throw error;
}
setIsInProgress(false);
From 30e9c405e20f9841c277cce16c4f3ebfee8dac79 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Thu, 29 Feb 2024 00:56:58 -0500
Subject: [PATCH 20/54] wip: cropping box follows mouse, need to remove
screenshot editor & help once submitting works
---
.../src/screenshotButton.tsx | 2 +-
.../src/screenshotEditor.tsx | 231 +++++++++++++++++-
.../src/screenshotEditorHelp.tsx | 112 +++++++++
.../src/screenshotWidget.tsx | 207 ++++++++++++++++
4 files changed, 541 insertions(+), 11 deletions(-)
create mode 100644 packages/feedback-screenshot/src/screenshotEditorHelp.tsx
create mode 100644 packages/feedback-screenshot/src/screenshotWidget.tsx
diff --git a/packages/feedback-screenshot/src/screenshotButton.tsx b/packages/feedback-screenshot/src/screenshotButton.tsx
index 4b72e80eac51..a2e853f2567d 100644
--- a/packages/feedback-screenshot/src/screenshotButton.tsx
+++ b/packages/feedback-screenshot/src/screenshotButton.tsx
@@ -2,7 +2,7 @@ import { h, render } from 'preact';
import { useState, useCallback, useEffect } from 'preact/hooks';
import { useTakeScreenshot } from './useTakeScreenshot';
import type { VNode } from 'preact';
-import { ScreenshotWidget } from './screenshotEditor';
+import { ScreenshotWidget } from './screenshotWidget';
type Props = {
croppingRef: HTMLDivElement;
diff --git a/packages/feedback-screenshot/src/screenshotEditor.tsx b/packages/feedback-screenshot/src/screenshotEditor.tsx
index 3c64c86db85a..65575b8e5534 100644
--- a/packages/feedback-screenshot/src/screenshotEditor.tsx
+++ b/packages/feedback-screenshot/src/screenshotEditor.tsx
@@ -1,22 +1,233 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { h, render } from 'preact';
import type { VNode } from 'preact';
+import { ScreenshotEditorHelp } from './screenshotEditorHelp';
+import { useEffect, useRef, useState } from 'preact/hooks';
+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;
type Props = {
screenshotImage: HTMLCanvasElement | null;
setScreenshotImage: (screenshot: HTMLCanvasElement | null) => void;
};
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+
+export interface Rect {
+ height: number;
+ width: number;
+ x: number;
+ y: number;
+}
export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props): VNode | null {
- const image = screenshotImage;
- return image ? (
+ // const image = screenshotImage;
+ // return image ? (
+ //
+ //
})
+ //
+ // ) : null;
+
+ // const Canvas = styled.canvas`
+ // position: absolute;
+ // cursor: crosshair;
+ // max-width: 100vw;
+ // max-height: 100vh;
+ // `;
+ // const Container = styled.div`
+ // position: fixed;
+ // z-index: 10000;
+ // height: 100vh;
+ // width: 100vw;
+ // top: 0;
+ // left: 0;
+ // background-color: rgba(240, 236, 243, 1);
+ // background-image: repeating-linear-gradient(
+ // 45deg,
+ // transparent,
+ // transparent 5px,
+ // rgba(0, 0, 0, 0.03) 5px,
+ // rgba(0, 0, 0, 0.03) 10px
+ // );
+ // `;
+
+ const getCanvasRenderSize = (width: number, height: number) => {
+ const maxWidth = WINDOW.innerWidth;
+ const maxHeight = WINDOW.innerHeight;
+
+ if (width > maxWidth) {
+ height = (maxWidth / width) * height;
+ width = maxWidth;
+ }
+
+ if (height > maxHeight) {
+ width = (maxHeight / height) * width;
+ height = maxHeight;
+ }
+
+ return { width, height };
+ };
+
+ interface Point {
+ x: number;
+ y: number;
+ }
+
+ const constructRect = (start: Point, end: Point) => {
+ return {
+ x: Math.min(start.x, end.x),
+ y: Math.min(start.y, end.y),
+ width: Math.abs(start.x - end.x),
+ height: Math.abs(start.y - end.y),
+ };
+ };
+
+ const canvasRef = useRef(screenshotImage);
+ const [isDraggingState, setIsDraggingState] = useState(true);
+ const currentRatio = useRef(1);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ // eslint-disable-next-line @sentry-internal/sdk/no-optional-chaining
+ const ctx = canvas?.getContext('2d');
+ let img = new Image();
+ const rectStart: { x: number; y: number } = { x: 0, y: 0 };
+ const rectEnd: { x: number; y: number } = { x: canvas?.width ?? 0, y: canvas?.height ?? 0 };
+ let isDragging = false;
+
+ function setCanvasSize() {
+ const renderSize = getCanvasRenderSize(img.width, img.height);
+ if (canvas) {
+ canvas.style.width = `${renderSize.width}px`;
+ canvas.style.height = `${renderSize.height}px`;
+ canvas.style.top = `${(WINDOW.innerHeight - renderSize.height) / 2}px`;
+ canvas.style.left = `${(WINDOW.innerWidth - renderSize.width) / 2}px`;
+ console.log(WINDOW.innerWidth, WINDOW.innerHeight, renderSize.width, renderSize.height);
+ }
+
+ // store it so we can translate the selection
+ currentRatio.current = renderSize.width / img.width;
+ }
+
+ function refreshCanvas() {
+ if (canvas && ctx) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, 0, 0);
+ }
+
+ if (!isDragging) {
+ return;
+ }
+
+ const rect = constructRect(rectStart, rectEnd);
+ if (canvas && ctx) {
+ // draw gray overlay around the selection
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
+ ctx.fillRect(0, 0, canvas.width, rect.y);
+ ctx.fillRect(0, rect.y, rect.x, rect.height);
+ ctx.fillRect(rect.x + rect.width, rect.y, canvas.width, rect.height);
+ ctx.fillRect(0, rect.y + rect.height, canvas.width, canvas.height);
+
+ // draw selection border
+ ctx.strokeStyle = '#79628c';
+ ctx.lineWidth = 6;
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
+ }
+ }
+
+ function submit(rect?: Rect) {
+ if (!rect) {
+ setScreenshotImage(canvas);
+ return;
+ }
+ // eslint-disable-next-line no-restricted-globals
+ const cutoutCanvas = document.createElement('canvas');
+ cutoutCanvas.width = rect.width;
+ cutoutCanvas.height = rect.height;
+ const cutoutCtx = cutoutCanvas.getContext('2d');
+ if (cutoutCtx && canvas) {
+ cutoutCtx.drawImage(canvas, rect.x, rect.y, rect.width, rect.height, 0, 0, rect.width, rect.height);
+ }
+
+ setScreenshotImage(cutoutCanvas);
+ img.src = cutoutCanvas.toDataURL();
+ }
+
+ function handleMouseDown(e: { offsetX: number; offsetY: number }) {
+ rectStart.x = Math.floor(e.offsetX / currentRatio.current);
+ rectStart.y = Math.floor(e.offsetY / currentRatio.current);
+ isDragging = true;
+ setIsDraggingState(true);
+ }
+
+ function handleMouseMove(e: { offsetX: number; offsetY: number }) {
+ rectEnd.x = Math.floor(e.offsetX / currentRatio.current);
+ rectEnd.y = Math.floor(e.offsetY / currentRatio.current);
+ refreshCanvas();
+ }
+
+ async function handleMouseUp() {
+ isDragging = false;
+ setIsDraggingState(false);
+ if (rectStart.x - rectEnd.x === 0 && rectStart.y - rectEnd.y === 0) {
+ // no selection
+ refreshCanvas();
+ return;
+ }
+ await submit(constructRect(rectStart, rectEnd));
+ }
+
+ async function handleEnterKey(e: { key: string }) {
+ if (e.key === 'Enter') {
+ await submit();
+ }
+ }
+
+ img.onload = () => {
+ if (canvas && ctx) {
+ canvas.width = img.width;
+ canvas.height = img.height;
+ setCanvasSize();
+ ctx.drawImage(img, 0, 0);
+ }
+ };
+
+ if (screenshotImage) {
+ img.src = screenshotImage.toDataURL();
+ }
+
+ WINDOW.addEventListener('resize', setCanvasSize, { passive: true });
+ canvas?.addEventListener('mousedown', handleMouseDown);
+ canvas?.addEventListener('mousemove', handleMouseMove);
+ canvas?.addEventListener('mouseup', handleMouseUp);
+ WINDOW.addEventListener('keydown', handleEnterKey);
+
+ return () => {
+ WINDOW.removeEventListener('resize', setCanvasSize);
+ canvas?.removeEventListener('mousedown', handleMouseDown);
+ canvas?.removeEventListener('mousemove', handleMouseMove);
+ canvas?.removeEventListener('mouseup', handleMouseUp);
+ WINDOW.removeEventListener('keydown', handleEnterKey);
+ };
+ }, [screenshotImage]);
+
+ return (
-
})
+
+ {/*
*/}
- ) : null;
+ );
}
diff --git a/packages/feedback-screenshot/src/screenshotEditorHelp.tsx b/packages/feedback-screenshot/src/screenshotEditorHelp.tsx
new file mode 100644
index 000000000000..af3ef7e30c55
--- /dev/null
+++ b/packages/feedback-screenshot/src/screenshotEditorHelp.tsx
@@ -0,0 +1,112 @@
+/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+import { h, render } from 'preact';
+import { useEffect, useState, useRef } from 'preact/hooks';
+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 Wrapper = styled.div`
+// position: fixed;
+// width: 100vw;
+// padding-top: 8px;
+// left: 0;
+// pointer-events: none;
+// display: flex;
+// justify-content: center;
+// transition: transform 0.2s ease-in-out;
+// transition-delay: 0.5s;
+// transform: translateY(0);
+// &[data-hide='true'] {
+// transition-delay: 0s;
+// transform: translateY(-100%);
+// }
+// `;
+
+// const Content = styled.div`
+// background-color: #231c3d;
+// border: 1px solid #ccc;
+// border-radius: 20px;
+// color: #fff;
+// font-size: 14px;
+// padding: 6px 24px;
+// box-shadow:
+// 0 0 0 1px rgba(0, 0, 0, 0.05),
+// 0 4px 16px rgba(0, 0, 0, 0.2);
+// `;
+
+export function ScreenshotEditorHelp({ hide }: { hide: boolean }) {
+ const [isHidden, setIsHidden] = useState(false);
+ const contentRef = useRef(null);
+
+ useEffect(() => {
+ let boundingRect: DOMRect;
+ if (contentRef.current) {
+ boundingRect = contentRef.current?.getBoundingClientRect();
+ }
+ const handleMouseMove = (e: MouseEvent) => {
+ const { clientX, clientY } = e;
+ const { left, bottom, right } = boundingRect;
+ const threshold = 50;
+ const isNearContent = clientX > left - threshold && clientX < right + threshold && clientY < bottom + threshold;
+ if (isNearContent) {
+ setIsHidden(true);
+ } else {
+ setIsHidden(false);
+ }
+ };
+
+ function handleResize() {
+ if (contentRef.current) {
+ boundingRect = contentRef.current?.getBoundingClientRect();
+ }
+ }
+
+ WINDOW.addEventListener('resize', handleResize);
+ WINDOW.addEventListener('mousemove', handleMouseMove);
+ return () => {
+ WINDOW.removeEventListener('resize', handleResize);
+ WINDOW.removeEventListener('mousemove', handleMouseMove);
+ };
+ }, []);
+
+ return (
+
+
+ {'Mark the problem on the screen (press "Enter" to skip)'}
+ Cancel
+ Confirm
+
+
+ );
+}
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
new file mode 100644
index 000000000000..b5564e23d477
--- /dev/null
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -0,0 +1,207 @@
+import type { VNode } from 'preact';
+import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
+import { GLOBAL_OBJ } from '@sentry/utils';
+import { h } from 'preact';
+import { once } from 'events';
+
+// 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;
+
+type Props = {
+ screenshotImage: HTMLCanvasElement | null;
+ setScreenshotImage: (screenshot: HTMLCanvasElement | null) => void;
+};
+
+interface Point {
+ x: number;
+ y: number;
+}
+
+export interface Rect {
+ height: number;
+ width: number;
+ x: number;
+ y: number;
+}
+
+const constructRect = (start: Point, end: Point) => {
+ return {
+ x: Math.min(start.x, end.x),
+ y: Math.min(start.y, end.y),
+ width: Math.abs(start.x - end.x),
+ height: Math.abs(start.y - end.y),
+ };
+};
+
+const getCanvasRenderSize = (width: number, height: number) => {
+ const maxWidth = WINDOW.innerWidth;
+ const maxHeight = WINDOW.innerHeight;
+
+ if (width > maxWidth) {
+ height = (maxWidth / width) * height;
+ width = maxWidth;
+ }
+
+ if (height > maxHeight) {
+ width = (maxHeight / height) * width;
+ height = maxHeight;
+ }
+
+ return { width, height };
+};
+
+export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props): VNode | null {
+ const canvasRef = useRef(null);
+ const currentRatio = useRef(1);
+ const [rectStart, setRectStart] = useState({ x: 0, y: 0 });
+ const [rectEnd, setRectEnd] = useState({ x: 0, y: 0 });
+ const [confirmCrop, setConfirmCrop] = useState(false);
+ const [crop, setCrop] = useState({ x: 0, y: 0, width: 0, height: 0 });
+ const imageRef = useRef(null);
+ const renderSize = getCanvasRenderSize(screenshotImage?.width ?? 0, screenshotImage?.height ?? 0);
+
+ useEffect(() => {
+ const imageCanvas = imageRef.current;
+ const ctx = imageCanvas?.getContext('2d');
+ const img = new Image();
+ if (screenshotImage) {
+ img.src = screenshotImage.toDataURL();
+ }
+ img.onload = () => {
+ if (imageCanvas && ctx) {
+ imageCanvas.width = img.width;
+ imageCanvas.height = img.height;
+ setCanvasSize(imageCanvas);
+ ctx.drawImage(img, 0, 0);
+ }
+ };
+ }, [screenshotImage]);
+
+ function setCanvasSize(canvas: HTMLCanvasElement) {
+ if (screenshotImage) {
+ canvas.style.width = `${renderSize.width}px`;
+ canvas.style.height = `${renderSize.height}px`;
+ canvas.style.top = `${(WINDOW.innerHeight - renderSize.height) / 2}px`;
+ canvas.style.left = `${(WINDOW.innerWidth - renderSize.width) / 2}px`;
+
+ setRectEnd({ x: renderSize.width, y: renderSize.height });
+ }
+ }
+
+ function refreshCanvas() {
+ const canvas = canvasRef.current;
+ const ctx = canvas?.getContext('2d');
+ if (canvas && ctx) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ const rect = constructRect(rectStart, rectEnd);
+ setCrop(rect);
+
+ // draw gray overlay around the selection
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
+ ctx.fillRect(0, 0, canvas.width, rect.y);
+ ctx.fillRect(0, rect.y, rect.x, rect.height);
+ ctx.fillRect(rect.x + rect.width, rect.y, canvas.width, rect.height);
+ ctx.fillRect(0, rect.y + rect.height, canvas.width, canvas.height);
+
+ // draw selection border
+ ctx.strokeStyle = '#ff0000';
+ ctx.lineWidth = 6;
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
+ }
+ }
+
+ const handleMouseMove = useCallback((e: { offsetX: number; offsetY: number }) => {
+ console.log('move');
+ setRectStart({
+ x: Math.floor(e.offsetX / currentRatio.current),
+ y: Math.floor(e.offsetY / currentRatio.current),
+ });
+ }, []);
+
+ const handleMouseUp = useCallback(() => {
+ console.log('up');
+ canvasRef.current?.removeEventListener('mousemove', handleMouseMove);
+ canvasRef.current?.removeEventListener('click', handleMouseUp);
+ setConfirmCrop(true);
+ }, []);
+
+ function onGrabButton() {
+ console.log('down');
+ setConfirmCrop(false);
+ canvasRef.current?.addEventListener('click', handleMouseUp);
+ canvasRef.current?.addEventListener('mousemove', handleMouseMove);
+ }
+
+ // canvasRef.current?.addEventListener('mousedown', onGrabButton);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (canvas) {
+ canvas.width = renderSize.width;
+ canvas.height = renderSize.height;
+ setCanvasSize(canvas);
+ }
+ }, [screenshotImage]);
+
+ useEffect(() => {
+ refreshCanvas();
+ }, [rectStart, rectEnd]);
+
+ function submit(rect?: Rect) {
+ const canvas = imageRef.current;
+ if (!rect) {
+ setScreenshotImage(canvas);
+ return;
+ }
+ // eslint-disable-next-line no-restricted-globals
+ const cutoutCanvas = document.createElement('canvas');
+ cutoutCanvas.width = rect.width;
+ cutoutCanvas.height = rect.height;
+ const cutoutCtx = cutoutCanvas.getContext('2d');
+ if (cutoutCtx && canvas) {
+ cutoutCtx.drawImage(canvas, rect.x, rect.y, rect.width, rect.height, 0, 0, rect.width, rect.height);
+ }
+
+ setScreenshotImage(cutoutCanvas);
+ screenshotImage = cutoutCanvas;
+ }
+
+ return (
+
+
+
+ {
+ e.preventDefault();
+ onGrabButton();
+ }}
+ onClick={e => {
+ e.preventDefault();
+ handleMouseUp();
+ }}
+ >
+
+
+
+ {
+ e.preventDefault();
+ submit(crop);
+ }}
+ >
+
+ );
+}
From a1771bd9c3fdf26055eb74466ea1a1b01faab87e Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Thu, 29 Feb 2024 12:44:35 -0500
Subject: [PATCH 21/54] cropping works for 1 cornergit add .
---
.../src/screenshotWidget.tsx | 94 +++++++++++--------
1 file changed, 55 insertions(+), 39 deletions(-)
diff --git a/packages/feedback-screenshot/src/screenshotWidget.tsx b/packages/feedback-screenshot/src/screenshotWidget.tsx
index b5564e23d477..e8f29d188e2c 100644
--- a/packages/feedback-screenshot/src/screenshotWidget.tsx
+++ b/packages/feedback-screenshot/src/screenshotWidget.tsx
@@ -2,7 +2,6 @@ import type { VNode } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { GLOBAL_OBJ } from '@sentry/utils';
import { h } from 'preact';
-import { once } from 'events';
// 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
@@ -60,31 +59,43 @@ export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props)
const [confirmCrop, setConfirmCrop] = useState(false);
const [crop, setCrop] = useState({ x: 0, y: 0, width: 0, height: 0 });
const imageRef = useRef(null);
- const renderSize = getCanvasRenderSize(screenshotImage?.width ?? 0, screenshotImage?.height ?? 0);
+ const [image, setImage] = useState(screenshotImage);
useEffect(() => {
const imageCanvas = imageRef.current;
const ctx = imageCanvas?.getContext('2d');
const img = new Image();
- if (screenshotImage) {
- img.src = screenshotImage.toDataURL();
- }
- img.onload = () => {
- if (imageCanvas && ctx) {
- imageCanvas.width = img.width;
- imageCanvas.height = img.height;
- setCanvasSize(imageCanvas);
- ctx.drawImage(img, 0, 0);
+
+ if (image) {
+ img.src = image.toDataURL();
+ const renderSize = getCanvasRenderSize(image.width, image.height);
+
+ img.onload = () => {
+ if (imageCanvas && ctx) {
+ imageCanvas.width = img.width;
+ imageCanvas.height = img.height;
+ setCanvasSize(imageCanvas);
+ ctx.drawImage(img, 0, 0);
+ }
+ };
+
+ const canvas = canvasRef.current;
+ if (canvas) {
+ canvas.width = renderSize.width;
+ canvas.height = renderSize.height;
+ setRectStart({ x: 0, y: 0 });
+ setCanvasSize(canvas);
}
- };
- }, [screenshotImage]);
+ }
+ }, [image]);
function setCanvasSize(canvas: HTMLCanvasElement) {
- if (screenshotImage) {
+ if (image) {
+ const renderSize = getCanvasRenderSize(image.width, image.height);
canvas.style.width = `${renderSize.width}px`;
canvas.style.height = `${renderSize.height}px`;
- canvas.style.top = `${(WINDOW.innerHeight - renderSize.height) / 2}px`;
- canvas.style.left = `${(WINDOW.innerWidth - renderSize.width) / 2}px`;
+ canvas.style.top = '0px';
+ canvas.style.left = '0px';
setRectEnd({ x: renderSize.width, y: renderSize.height });
}
@@ -135,17 +146,6 @@ export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props)
canvasRef.current?.addEventListener('mousemove', handleMouseMove);
}
- // canvasRef.current?.addEventListener('mousedown', onGrabButton);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (canvas) {
- canvas.width = renderSize.width;
- canvas.height = renderSize.height;
- setCanvasSize(canvas);
- }
- }, [screenshotImage]);
-
useEffect(() => {
refreshCanvas();
}, [rectStart, rectEnd]);
@@ -154,6 +154,7 @@ export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props)
const canvas = imageRef.current;
if (!rect) {
setScreenshotImage(canvas);
+ setImage(canvas);
return;
}
// eslint-disable-next-line no-restricted-globals
@@ -166,7 +167,7 @@ export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props)
}
setScreenshotImage(cutoutCanvas);
- screenshotImage = cutoutCanvas;
+ setImage(cutoutCanvas);
}
return (
@@ -187,21 +188,36 @@ export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props)
- {
- e.preventDefault();
- submit(crop);
- }}
- >
+ >
+ {
+ e.preventDefault();
+ setRectStart({ x: 0, y: 0 });
+ setConfirmCrop(false);
+ }}
+ >
+ Cancel
+
+ {
+ e.preventDefault();
+ submit(crop);
+ setConfirmCrop(false);
+ }}
+ >
+ Submit
+
+
);
}
From ea96f76d37a57d62cb58c32439d80932922b8a2d Mon Sep 17 00:00:00 2001
From: Ryan Albrecht
Date: Thu, 29 Feb 2024 10:28:51 -0800
Subject: [PATCH 22/54] Ryan953/feedback async (#10683)
This is building off a version of
https://github.com/getsentry/sentry-javascript/pull/10590, specifically
working towards async loading of the Dialog component, and rewriting it
in preact in order to host and interact with the screenshot feature in
an easier way.
Setup in my test app is (gives screenshots):
```
import { feedback2Integration, feedback2ModalIntegration, feedback2ScreenshotIntegration } from "@sentry-internal/feedback2";
integrations: [
feedback2Integration({
colorScheme: 'light',
isNameRequired: false,
isEmailRequired: false,
}),
feedback2ModalIntegration(),
feedback2ScreenshotIntegration(),
]
```
---
.size-limit.js | 21 ++
package.json | 1 +
packages/feedback2/.eslintignore | 2 +
packages/feedback2/.eslintrc.js | 25 ++
packages/feedback2/.gitignore | 4 +
packages/feedback2/LICENSE | 14 +
packages/feedback2/README.md | 20 ++
packages/feedback2/jest.config.js | 6 +
packages/feedback2/package.json | 64 ++++
packages/feedback2/rollup.bundle.config.mjs | 15 +
packages/feedback2/rollup.npm.config.mjs | 20 ++
packages/feedback2/src/constants/index.ts | 27 ++
packages/feedback2/src/constants/theme.ts | 55 ++++
.../src/core/components/Actor.css.ts | 63 ++++
.../feedback2/src/core/components/Actor.ts | 48 +++
.../src/core/components/FeedbackIcon.ts | 49 ++++
.../feedback2/src/core/createMainStyles.ts | 73 +++++
packages/feedback2/src/core/integration.ts | 277 ++++++++++++++++++
packages/feedback2/src/core/sendFeedback.ts | 142 +++++++++
packages/feedback2/src/index.ts | 7 +
.../src/modal/components/Dialog.css.ts | 224 ++++++++++++++
.../src/modal/components/DialogContainer.tsx | 58 ++++
.../src/modal/components/DialogContent.tsx | 21 ++
.../src/modal/components/DialogHeader.tsx | 32 ++
.../feedback2/src/modal/components/Form.tsx | 233 +++++++++++++++
.../feedback2/src/modal/components/Icon.ts | 49 ++++
.../src/modal/components/SentryLogo.ts | 53 ++++
.../src/modal/components/SuccessIcon.ts | 54 ++++
packages/feedback2/src/modal/createDialog.tsx | 94 ++++++
packages/feedback2/src/modal/integration.ts | 22 ++
.../components/ScreenshotEditor.tsx | 61 ++++
.../components/ScreenshotInput.css.ts | 52 ++++
.../components/useTakeScreenshot.tsx | 44 +++
.../feedback2/src/screenshot/createInput.ts | 33 +++
.../feedback2/src/screenshot/integration.ts | 22 ++
packages/feedback2/src/types/config.ts | 170 +++++++++++
packages/feedback2/src/types/form.ts | 8 +
packages/feedback2/src/types/index.ts | 101 +++++++
packages/feedback2/src/types/theme.ts | 118 ++++++++
packages/feedback2/src/util/debug-build.ts | 8 +
packages/feedback2/src/util/mergeOptions.ts | 42 +++
.../src/util/prepareFeedbackEvent.ts | 43 +++
.../feedback2/src/util/setAttributesNS.ts | 9 +
packages/feedback2/src/util/validate.ts | 24 ++
packages/feedback2/tsconfig.json | 17 ++
packages/feedback2/tsconfig.test.json | 15 +
packages/feedback2/tsconfig.types.json | 10 +
packages/types/src/feedback.ts | 13 +-
packages/types/src/index.ts | 4 +-
packages/types/src/user.ts | 7 -
50 files changed, 2564 insertions(+), 10 deletions(-)
create mode 100644 packages/feedback2/.eslintignore
create mode 100644 packages/feedback2/.eslintrc.js
create mode 100644 packages/feedback2/.gitignore
create mode 100644 packages/feedback2/LICENSE
create mode 100644 packages/feedback2/README.md
create mode 100644 packages/feedback2/jest.config.js
create mode 100644 packages/feedback2/package.json
create mode 100644 packages/feedback2/rollup.bundle.config.mjs
create mode 100644 packages/feedback2/rollup.npm.config.mjs
create mode 100644 packages/feedback2/src/constants/index.ts
create mode 100644 packages/feedback2/src/constants/theme.ts
create mode 100644 packages/feedback2/src/core/components/Actor.css.ts
create mode 100644 packages/feedback2/src/core/components/Actor.ts
create mode 100644 packages/feedback2/src/core/components/FeedbackIcon.ts
create mode 100644 packages/feedback2/src/core/createMainStyles.ts
create mode 100644 packages/feedback2/src/core/integration.ts
create mode 100644 packages/feedback2/src/core/sendFeedback.ts
create mode 100644 packages/feedback2/src/index.ts
create mode 100644 packages/feedback2/src/modal/components/Dialog.css.ts
create mode 100644 packages/feedback2/src/modal/components/DialogContainer.tsx
create mode 100644 packages/feedback2/src/modal/components/DialogContent.tsx
create mode 100644 packages/feedback2/src/modal/components/DialogHeader.tsx
create mode 100644 packages/feedback2/src/modal/components/Form.tsx
create mode 100644 packages/feedback2/src/modal/components/Icon.ts
create mode 100644 packages/feedback2/src/modal/components/SentryLogo.ts
create mode 100644 packages/feedback2/src/modal/components/SuccessIcon.ts
create mode 100644 packages/feedback2/src/modal/createDialog.tsx
create mode 100644 packages/feedback2/src/modal/integration.ts
create mode 100644 packages/feedback2/src/screenshot/components/ScreenshotEditor.tsx
create mode 100644 packages/feedback2/src/screenshot/components/ScreenshotInput.css.ts
create mode 100644 packages/feedback2/src/screenshot/components/useTakeScreenshot.tsx
create mode 100644 packages/feedback2/src/screenshot/createInput.ts
create mode 100644 packages/feedback2/src/screenshot/integration.ts
create mode 100644 packages/feedback2/src/types/config.ts
create mode 100644 packages/feedback2/src/types/form.ts
create mode 100644 packages/feedback2/src/types/index.ts
create mode 100644 packages/feedback2/src/types/theme.ts
create mode 100644 packages/feedback2/src/util/debug-build.ts
create mode 100644 packages/feedback2/src/util/mergeOptions.ts
create mode 100644 packages/feedback2/src/util/prepareFeedbackEvent.ts
create mode 100644 packages/feedback2/src/util/setAttributesNS.ts
create mode 100644 packages/feedback2/src/util/validate.ts
create mode 100644 packages/feedback2/tsconfig.json
create mode 100644 packages/feedback2/tsconfig.test.json
create mode 100644 packages/feedback2/tsconfig.types.json
diff --git a/.size-limit.js b/.size-limit.js
index 5e94a923e656..cbc2f411b628 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -61,6 +61,27 @@ module.exports = [
gzip: true,
limit: '50 KB',
},
+ {
+ name: '@sentry/browser (incl. feedback2Integration) - Webpack (gzipped)',
+ path: 'packages/browser/build/npm/esm/index.js',
+ import: '{ init, feedback2Integration }',
+ gzip: true,
+ limit: '50 KB',
+ },
+ {
+ name: '@sentry/browser (incl. feedback2ModalIntegration) - Webpack (gzipped)',
+ path: 'packages/browser/build/npm/esm/index.js',
+ import: '{ init, feedback2Integration, feedback2ModalIntegration }',
+ gzip: true,
+ limit: '50 KB',
+ },
+ {
+ name: '@sentry/browser (incl. feedback2ScreenshotIntegration) - Webpack (gzipped)',
+ path: 'packages/browser/build/npm/esm/index.js',
+ import: '{ init, feedback2Integration, feedback2ModalIntegration, feedback2ScreenshotIntegration }',
+ gzip: true,
+ limit: '50 KB',
+ },
{
name: '@sentry/browser (incl. sendFeedback) - Webpack (gzipped)',
path: 'packages/browser/build/npm/esm/index.js',
diff --git a/package.json b/package.json
index ef36567d47e3..414d4d00713b 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"packages/eslint-plugin-sdk",
"packages/feedback",
"packages/feedback-screenshot",
+ "packages/feedback2",
"packages/gatsby",
"packages/hub",
"packages/integrations",
diff --git a/packages/feedback2/.eslintignore b/packages/feedback2/.eslintignore
new file mode 100644
index 000000000000..b38db2f296ff
--- /dev/null
+++ b/packages/feedback2/.eslintignore
@@ -0,0 +1,2 @@
+node_modules/
+build/
diff --git a/packages/feedback2/.eslintrc.js b/packages/feedback2/.eslintrc.js
new file mode 100644
index 000000000000..ef2ed992265c
--- /dev/null
+++ b/packages/feedback2/.eslintrc.js
@@ -0,0 +1,25 @@
+// Note: All paths are relative to the directory in which eslint is being run, rather than the directory where this file
+// lives
+
+// ESLint config docs: https://eslint.org/docs/user-guide/configuring/
+
+module.exports = {
+ extends: ['../../.eslintrc.js'],
+ overrides: [
+ {
+ files: ['src/**/*.ts', 'src/**/*.tsx'],
+ rules: {
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
+ },
+ },
+ {
+ files: ['jest.setup.ts', 'jest.config.ts'],
+ parserOptions: {
+ project: ['tsconfig.test.json'],
+ },
+ rules: {
+ 'no-console': 'off',
+ },
+ },
+ ],
+};
diff --git a/packages/feedback2/.gitignore b/packages/feedback2/.gitignore
new file mode 100644
index 000000000000..363d3467c6fa
--- /dev/null
+++ b/packages/feedback2/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/*.tgz
+.eslintcache
+build
diff --git a/packages/feedback2/LICENSE b/packages/feedback2/LICENSE
new file mode 100644
index 000000000000..d11896ba1181
--- /dev/null
+++ b/packages/feedback2/LICENSE
@@ -0,0 +1,14 @@
+Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/feedback2/README.md b/packages/feedback2/README.md
new file mode 100644
index 000000000000..fb5b20400a71
--- /dev/null
+++ b/packages/feedback2/README.md
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+# Sentry Integration for Feedback
+
+This SDK is **considered experimental and in a beta state**. It may experience breaking changes, and may be discontinued at any time. Please reach out on
+[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback/concerns.
+
+To view Feedback in Sentry, your [Sentry organization must be an early adopter](https://docs.sentry.io/product/accounts/early-adopter-features/).
+
+## Installation
+
+Please read the [offical integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/) for installation instructions.
+
+## Configuration
+
+The Feedback integration is highly customizable, please read the [official integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/configuration/) for the most up-to-date configuration options.
diff --git a/packages/feedback2/jest.config.js b/packages/feedback2/jest.config.js
new file mode 100644
index 000000000000..cd02790794a7
--- /dev/null
+++ b/packages/feedback2/jest.config.js
@@ -0,0 +1,6 @@
+const baseConfig = require('../../jest/jest.config.js');
+
+module.exports = {
+ ...baseConfig,
+ testEnvironment: 'jsdom',
+};
diff --git a/packages/feedback2/package.json b/packages/feedback2/package.json
new file mode 100644
index 000000000000..416ba4436049
--- /dev/null
+++ b/packages/feedback2/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "@sentry-internal/feedback2",
+ "version": "7.100.0",
+ "description": "Sentry SDK integration for user feedback",
+ "repository": "git://github.com/getsentry/sentry-javascript.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback",
+ "author": "Sentry",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "files": [
+ "cjs",
+ "esm",
+ "types",
+ "types-ts3.8"
+ ],
+ "main": "build/npm/cjs/index.js",
+ "module": "build/npm/esm/index.js",
+ "types": "build/npm/types/index.d.ts",
+ "typesVersions": {
+ "<4.9": {
+ "build/npm/types/index.d.ts": [
+ "build/npm/types-ts3.8/index.d.ts"
+ ]
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@sentry/core": "7.100.0",
+ "@sentry/types": "7.100.0",
+ "@sentry/utils": "7.100.0",
+ "preact": "^10.19.4",
+ "preact-compat": "^3.19.0"
+ },
+ "scripts": {
+ "build": "run-p build:transpile build:types build:bundle",
+ "build:transpile": "rollup -c rollup.npm.config.mjs",
+ "build:bundle": "rollup -c rollup.bundle.config.mjs",
+ "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: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",
+ "build:bundle:watch": "yarn build:bundle --watch",
+ "build:types:watch": "tsc -p tsconfig.types.json --watch",
+ "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm",
+ "circularDepCheck": "madge --circular src/index.ts",
+ "clean": "rimraf build sentry-feedback-*.tgz",
+ "fix": "eslint . --format stylish --fix",
+ "lint": "eslint . --format stylish",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push --sig"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sideEffects": false
+}
diff --git a/packages/feedback2/rollup.bundle.config.mjs b/packages/feedback2/rollup.bundle.config.mjs
new file mode 100644
index 000000000000..32c9684415bf
--- /dev/null
+++ b/packages/feedback2/rollup.bundle.config.mjs
@@ -0,0 +1,15 @@
+import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils';
+
+export default makeBundleConfigVariants(
+ makeBaseBundleConfig({
+ bundleType: 'addon',
+ entrypoints: ['src/index.ts'],
+ jsVersion: 'es6',
+ licenseTitle: '@sentry-internal/feedback2',
+ outputFileBase: () => 'bundles/feedback2',
+ sucrase: {
+ jsxPragma: 'h',
+ jsxFragmentPragma: 'Fragment',
+ },
+ }),
+);
diff --git a/packages/feedback2/rollup.npm.config.mjs b/packages/feedback2/rollup.npm.config.mjs
new file mode 100644
index 000000000000..80f2406e78db
--- /dev/null
+++ b/packages/feedback2/rollup.npm.config.mjs
@@ -0,0 +1,20 @@
+import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
+
+export default makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ hasBundles: true,
+ packageSpecificConfig: {
+ output: {
+ // set exports to 'named' or 'auto' so that rollup doesn't warn
+ exports: 'named',
+ // set preserveModules to false because for Feedback we actually want
+ // to bundle everything into one file.
+ preserveModules: false,
+ },
+ },
+ sucrase: {
+ jsxPragma: 'h',
+ jsxFragmentPragma: 'Fragment',
+ },
+ }),
+);
diff --git a/packages/feedback2/src/constants/index.ts b/packages/feedback2/src/constants/index.ts
new file mode 100644
index 000000000000..9804fdedf431
--- /dev/null
+++ b/packages/feedback2/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/feedback2/src/constants/theme.ts b/packages/feedback2/src/constants/theme.ts
new file mode 100644
index 000000000000..7fff31f48964
--- /dev/null
+++ b/packages/feedback2/src/constants/theme.ts
@@ -0,0 +1,55 @@
+const LIGHT_BACKGROUND = '#ffffff';
+const INHERIT = 'inherit';
+const SUBMIT_COLOR = 'rgba(108, 95, 199, 1)';
+
+export const LIGHT_THEME = {
+ fontFamily: "system-ui, 'Helvetica Neue', Arial, sans-serif",
+ fontSize: '14px',
+
+ background: LIGHT_BACKGROUND,
+ backgroundHover: '#f6f6f7',
+ foreground: '#2b2233',
+ border: '1.5px solid rgba(41, 35, 47, 0.13)',
+ borderRadius: '12px',
+ boxShadow: '0px 4px 24px 0px rgba(43, 34, 51, 0.12)',
+
+ success: '#268d75',
+ error: '#df3338',
+
+ submitBackground: 'rgba(88, 74, 192, 1)',
+ submitBackgroundHover: SUBMIT_COLOR,
+ submitBorder: SUBMIT_COLOR,
+ submitOutlineFocus: '#29232f',
+ submitForeground: LIGHT_BACKGROUND,
+ submitForegroundHover: LIGHT_BACKGROUND,
+
+ cancelBackground: 'transparent',
+ cancelBackgroundHover: 'var(--background-hover)',
+ cancelBorder: 'var(--border)',
+ cancelOutlineFocus: 'var(--input-outline-focus)',
+ cancelForeground: 'var(--foreground)',
+ cancelForegroundHover: 'var(--foreground)',
+
+ inputBackground: INHERIT,
+ inputForeground: INHERIT,
+ inputBorder: 'var(--border)',
+ inputOutlineFocus: SUBMIT_COLOR,
+
+ formBorderRadius: '20px',
+ formContentBorderRadius: '6px',
+};
+
+export const DEFAULT_THEME = {
+ light: LIGHT_THEME,
+ dark: {
+ ...LIGHT_THEME,
+
+ background: '#29232f',
+ backgroundHover: '#352f3b',
+ foreground: '#ebe6ef',
+ border: '1.5px solid rgba(235, 230, 239, 0.15)',
+
+ success: '#2da98c',
+ error: '#f55459',
+ },
+};
diff --git a/packages/feedback2/src/core/components/Actor.css.ts b/packages/feedback2/src/core/components/Actor.css.ts
new file mode 100644
index 000000000000..9ae8df503cc9
--- /dev/null
+++ b/packages/feedback2/src/core/components/Actor.css.ts
@@ -0,0 +1,63 @@
+import { DOCUMENT } from '../../constants';
+
+/**
+ * Creates