SALEOR-2064 Add generic error tracker with Sentry adapter (#956)

* Add generic error tracker with Sentry extension

* Add Sentry webpack plugin

* Update variable names and README

* Update deploy-staging template

* Update changelog
This commit is contained in:
Jakub Majorek 2021-01-22 15:05:26 +01:00 committed by GitHub
parent 10131163c1
commit 383055a2de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 451 additions and 23 deletions

View file

@ -11,6 +11,12 @@ jobs:
API_URI: https://master.staging.saleor.cloud/graphql/ API_URI: https://master.staging.saleor.cloud/graphql/
APP_MOUNT_URI: /dashboard/ APP_MOUNT_URI: /dashboard/
STATIC_URL: /dashboard/static/ STATIC_URL: /dashboard/static/
SENTRY_ORG: sentry
SENTRY_PROJECT: dashboard
SENTRY_URL_PREFIX: "~/static"
ENVIRONMENT: master-staging
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Package - name: Package
@ -41,4 +47,3 @@ jobs:
aws s3 sync build/dashboard s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/static/ aws s3 sync build/dashboard s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/static/
aws s3 cp build/dashboard/index.html s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/ aws s3 cp build/dashboard/index.html s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/
aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_STAGING_CF_DIST_ID }} --paths "/dashboard*" aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_STAGING_CF_DIST_ID }} --paths "/dashboard*"

View file

@ -17,6 +17,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add reference attributes - #917 by @orzechdev - Add reference attributes - #917 by @orzechdev
- Add product reference attributes - #948 by @orzechdev - Add product reference attributes - #948 by @orzechdev
- Drop descriptionJson and contentJson fields - #950 by @jwm0 - Drop descriptionJson and contentJson fields - #950 by @jwm0
- Add error tracking with Sentry adapter - #956 by @jwm0
# 2.11.1 # 2.11.1

View file

@ -61,7 +61,7 @@ $ npm i
### Configuration ### Configuration
There are two environment variables available for configuration: There following environment variables are available for configuration:
- `API_URI` (required) - URI of a running instance of Saleor GraphQL API. - `API_URI` (required) - URI of a running instance of Saleor GraphQL API.
If you are running Saleor locally with the default settings, set `API_URI` to: `http://localhost:8000/graphql/`. If you are running Saleor locally with the default settings, set `API_URI` to: `http://localhost:8000/graphql/`.
@ -88,4 +88,43 @@ To build the application bundle run:
$ npm run build $ npm run build
``` ```
### Error Tracking
Saleor Dashboard is using a generic error tracking wrapper function that takes care of the most popular use cases:
- initializing the tracker
- capturing exceptions and (optionally) displaying the event id
- setting basic user data (this is opt-in and disabled by default)
By default it ships with a Sentry adapter but any kind of error tracking software can be used by creating a custom adapter (using Sentry and TS types as an example).
Example:
```javascript
// src/services/errorTracking/index.ts
import { CustomAdapter } from "./adapters/";
const errorTracker = ErrorTrackerFactory(CustomAdapter(config));
```
##### Usage with Sentry adapter:
Sentry is used as the default tracker so no changes in code are necessary and the configuration is done via environment variables.
The following environment variables are available:
```
# Required
SENTRY_DSN=
# Optional
# https://docs.sentry.io/product/cli/configuration/
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_URL_PREFIX=
ENVIRONMENT=
```
#### Crafted with ❤️ by [Mirumee Software](https://mirumee.com) #### Crafted with ❤️ by [Mirumee Software](https://mirumee.com)

120
package-lock.json generated
View file

@ -3153,6 +3153,120 @@
"any-observable": "^0.3.0" "any-observable": "^0.3.0"
} }
}, },
"@sentry/browser": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.0.0.tgz",
"integrity": "sha512-R4+MHb5FyVZCz3EVnaquvT1mwOM2MWP4gBqjYEADY5m0XWoHiJf0skFkWt8iEKJanzGbhl4PMb9gHuJj6YfVLw==",
"requires": {
"@sentry/core": "6.0.0",
"@sentry/types": "6.0.0",
"@sentry/utils": "6.0.0",
"tslib": "^1.9.3"
}
},
"@sentry/cli": {
"version": "1.61.0",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.61.0.tgz",
"integrity": "sha512-pHEhqP1bB4sdO7N5ow/IkRBrPbKT9HZRinq4PhTVIvmG+NW4VVuVZ6k4tlbp+JXmzMcUc/iXynVkTL7zJIlTQw==",
"dev": true,
"requires": {
"https-proxy-agent": "^5.0.0",
"mkdirp": "^0.5.5",
"node-fetch": "^2.6.0",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0"
},
"dependencies": {
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dev": true,
"requires": {
"agent-base": "6",
"debug": "4"
}
}
}
},
"@sentry/core": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.0.0.tgz",
"integrity": "sha512-afAiOachs/WfGWc9LsJBFnJMhqQVENyzfSMnf7sLRvxPAw8n7IrXY0R09MKmG0SlAnTKN2pWoQFzFF+J3NuHBA==",
"requires": {
"@sentry/hub": "6.0.0",
"@sentry/minimal": "6.0.0",
"@sentry/types": "6.0.0",
"@sentry/utils": "6.0.0",
"tslib": "^1.9.3"
}
},
"@sentry/hub": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.0.0.tgz",
"integrity": "sha512-s8IsW6LvEH7ACnniQcxxb/9uEyjmoQ/TAoryTJN2qyPzzrHTw8NCyMuJvK+8ivUvRViz5AvtuOFf8AJlh9lzeA==",
"requires": {
"@sentry/types": "6.0.0",
"@sentry/utils": "6.0.0",
"tslib": "^1.9.3"
}
},
"@sentry/minimal": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.0.0.tgz",
"integrity": "sha512-daYdEzTr+ERMwViu6RpWHOfk0oZrSNqdx+7bejTqmFHqO4pt+9ZrMiw3vinL+MWQcKXwD95uXBz6O/ryrVdPtg==",
"requires": {
"@sentry/hub": "6.0.0",
"@sentry/types": "6.0.0",
"tslib": "^1.9.3"
}
},
"@sentry/react": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.0.0.tgz",
"integrity": "sha512-GYX110NSodd8wGUbnyxemndTijM+U7dI/WjFSPOyJdLB2hzzPjJ9kUqtuobT/JlGzbWE2278WysAuySne6bUGw==",
"requires": {
"@sentry/browser": "6.0.0",
"@sentry/minimal": "6.0.0",
"@sentry/types": "6.0.0",
"@sentry/utils": "6.0.0",
"hoist-non-react-statics": "^3.3.2",
"tslib": "^1.9.3"
},
"dependencies": {
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
}
}
},
"@sentry/types": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.0.tgz",
"integrity": "sha512-yueRSRGPCahuju/UMdtOt8LIIncbpwLINQd9Q8E4OXtoPpMHR6Oun8sMKCPd+Wq3piI5yRDzKkGCl+sH7mHVrA=="
},
"@sentry/utils": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.0.tgz",
"integrity": "sha512-dMMWOT69bQ4CF1R33dOnXIOyiHRWsUAON3nFVljV1JNNTDA69YwaF9f5FIT0DKpO4qhgTlElsm8WgHI9prAVEQ==",
"requires": {
"@sentry/types": "6.0.0",
"tslib": "^1.9.3"
}
},
"@sentry/webpack-plugin": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-1.14.0.tgz",
"integrity": "sha512-1cS99mnHqASYtMlHi2J107p6x3lfC5NmLOgA0iI6avaaFes8RTQMlW8YT2CyrvhtQod1bViPZOlh3NOVC8vnOA==",
"dev": true,
"requires": {
"@sentry/cli": "^1.58.0"
}
},
"@sindresorhus/fnv1a": { "@sindresorhus/fnv1a": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-1.2.0.tgz",
@ -18614,6 +18728,12 @@
"ipaddr.js": "1.9.0" "ipaddr.js": "1.9.0"
} }
}, },
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"prr": { "prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",

View file

@ -26,6 +26,7 @@
"@material-ui/icons": "^4.5.1", "@material-ui/icons": "^4.5.1",
"@material-ui/styles": "^4.5.2", "@material-ui/styles": "^4.5.2",
"@saleor/macaw-ui": "^0.1.1-9", "@saleor/macaw-ui": "^0.1.1-9",
"@sentry/react": "^6.0.0",
"apollo": "^2.21.2", "apollo": "^2.21.2",
"apollo-cache-inmemory": "^1.6.5", "apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8", "apollo-client": "^2.6.8",
@ -92,6 +93,7 @@
"@pollyjs/adapter-node-http": "^5.0.0", "@pollyjs/adapter-node-http": "^5.0.0",
"@pollyjs/core": "^5.0.0", "@pollyjs/core": "^5.0.0",
"@pollyjs/persister-fs": "^5.0.0", "@pollyjs/persister-fs": "^5.0.0",
"@sentry/webpack-plugin": "^1.14.0",
"@storybook/addon-storyshots": "^5.2.8", "@storybook/addon-storyshots": "^5.2.8",
"@storybook/react": "^5.1.9", "@storybook/react": "^5.1.9",
"@testing-library/react-hooks": "^1.1.0", "@testing-library/react-hooks": "^1.1.0",
@ -226,6 +228,7 @@
"test:e2e:dev": "start-server-and-test start http://localhost:9000 cy:open", "test:e2e:dev": "start-server-and-test start http://localhost:9000 cy:open",
"test": "jest src/", "test": "jest src/",
"transpile-messages": "node scripts/transpile-tx.js", "transpile-messages": "node scripts/transpile-tx.js",
"lint": "npx eslint \"src/**/*.@(tsx|ts|jsx|js)\" --fix ; npx prettier --check \"src/**/*.@(tsx|ts|jsx|js)\" --write" "lint": "npx eslint \"src/**/*.@(tsx|ts|jsx|js)\" --fix ; npx prettier --check \"src/**/*.@(tsx|ts|jsx|js)\" --write",
"postbuild": "rimraf ./build/**/*.js.map"
} }
} }

View file

@ -4,6 +4,7 @@ import { User } from "@saleor/fragments/types/User";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getMutationStatus } from "@saleor/misc"; import { getMutationStatus } from "@saleor/misc";
import errorTracker from "@saleor/services/errorTracking";
import { import {
isSupported as isCredentialsManagementAPISupported, isSupported as isCredentialsManagementAPISupported,
login as loginWithCredentialsManagementAPI, login as loginWithCredentialsManagementAPI,
@ -52,13 +53,22 @@ export function useAuthProvider(
}, []); }, []);
useEffect(() => { useEffect(() => {
if (userContext && !userContext.isStaff) { if (userContext) {
logout(); const { id, email, firstName, lastName } = userContext;
notify({ errorTracker.setUserData({
status: "error", email,
text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess), id,
title: intl.formatMessage(commonMessages.insufficientPermissions) username: `${firstName} ${lastName}`
}); });
if (!userContext.isStaff) {
logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess),
title: intl.formatMessage(commonMessages.insufficientPermissions)
});
}
} }
}, [userContext]); }, [userContext]);

View file

@ -239,8 +239,11 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
</div> </div>
<main className={classes.view}> <main className={classes.view}>
{appState.error {appState.error
? appState.error === "unhandled" && ( ? appState.error.type === "unhandled" && (
<ErrorPage onBack={handleErrorBack} /> <ErrorPage
id={appState.error.id}
onBack={handleErrorBack}
/>
) )
: children} : children}
</main> </main>

View file

@ -7,6 +7,7 @@ import SVG from "react-inlinesvg";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
export interface ErrorPageProps { export interface ErrorPageProps {
id?: string | null;
onBack: () => void; onBack: () => void;
} }
@ -31,6 +32,9 @@ const useStyles = makeStyles(
margin: "0 auto", margin: "0 auto",
width: 830 width: 830
}, },
errorId: {
marginTop: theme.spacing(3)
},
innerContainer: { innerContainer: {
[theme.breakpoints.down("sm")]: { [theme.breakpoints.down("sm")]: {
order: 1, order: 1,
@ -58,7 +62,7 @@ const useStyles = makeStyles(
); );
const ErrorPage: React.FC<ErrorPageProps> = props => { const ErrorPage: React.FC<ErrorPageProps> = props => {
const { onBack } = props; const { onBack, id } = props;
const classes = useStyles(props); const classes = useStyles(props);
@ -79,6 +83,11 @@ const ErrorPage: React.FC<ErrorPageProps> = props => {
<Typography> <Typography>
<FormattedMessage defaultMessage="Don't worry, everything is gonna be fine" /> <FormattedMessage defaultMessage="Don't worry, everything is gonna be fine" />
</Typography> </Typography>
{!!id && (
<Typography variant="subtitle2" className={classes.errorId}>
Error ID: {id}
</Typography>
)}
</div> </div>
<div> <div>
<Button <Button

View file

@ -4,16 +4,24 @@ export type AppStateReducerActionType = "displayError" | "displayLoader";
export interface AppStateReducerAction { export interface AppStateReducerAction {
payload: Partial<{ payload: Partial<{
error: AppError; error: AppError["type"];
errorId: AppError["id"];
value: boolean; value: boolean;
}>; }>;
type: AppStateReducerActionType; type: AppStateReducerActionType;
} }
function displayError(prevState: IAppState, error: AppError): IAppState { function displayError(
prevState: IAppState,
errorType: AppError["type"],
errorId?: AppError["id"]
): IAppState {
return { return {
...prevState, ...prevState,
error, error: {
id: errorId,
type: errorType
},
loading: false loading: false
}; };
} }
@ -31,7 +39,11 @@ function reduceAppState(
): IAppState { ): IAppState {
switch (action.type) { switch (action.type) {
case "displayError": case "displayError":
return displayError(prevState, action.payload.error); return displayError(
prevState,
action.payload.error,
action.payload.errorId
);
case "displayLoader": case "displayLoader":
return displayLoader(prevState, action.payload.value); return displayLoader(prevState, action.payload.value);
default: default:

View file

@ -1,4 +1,7 @@
export type AppError = "unhandled"; export interface AppError {
type: "unhandled";
id: string | null | undefined;
}
interface IAppState { interface IAppState {
error: AppError | null; error: AppError | null;

View file

@ -54,6 +54,7 @@ import PermissionGroupSection from "./permissionGroups";
import PluginsSection from "./plugins"; import PluginsSection from "./plugins";
import ProductSection from "./products"; import ProductSection from "./products";
import ProductTypesSection from "./productTypes"; import ProductTypesSection from "./productTypes";
import errorTracker from "./services/errorTracking";
import ShippingSection from "./shipping"; import ShippingSection from "./shipping";
import SiteSettingsSection from "./siteSettings"; import SiteSettingsSection from "./siteSettings";
import StaffSection from "./staff"; import StaffSection from "./staff";
@ -67,6 +68,8 @@ if (process.env.GTM_ID) {
TagManager.initialize({ gtmId: GTM_ID }); TagManager.initialize({ gtmId: GTM_ID });
} }
errorTracker.init();
// DON'T TOUCH THIS // DON'T TOUCH THIS
// These are separate clients and do not share configs between themselves // These are separate clients and do not share configs between themselves
// so we need to explicitly set them // so we need to explicitly set them
@ -159,14 +162,17 @@ const Routes: React.FC = () => {
{homePageLoaded ? ( {homePageLoaded ? (
<AppLayout> <AppLayout>
<ErrorBoundary <ErrorBoundary
onError={() => onError={e => {
const errorId = errorTracker.captureException(e);
dispatchAppState({ dispatchAppState({
payload: { payload: {
errorId,
error: "unhandled" error: "unhandled"
}, },
type: "displayError" type: "displayError"
}) });
} }}
> >
<Switch> <Switch>
<SectionRoute exact path="/" component={HomePage} /> <SectionRoute exact path="/" component={HomePage} />

View file

@ -0,0 +1,33 @@
import * as Sentry from "@sentry/react";
import { TrackerMethods } from "../types";
interface Config {
dsn: string;
environment?: string;
}
export const SentryAdapter = (config: Config): TrackerMethods => {
const init: TrackerMethods["init"] = () => {
if (config?.dsn) {
Sentry.init({
dsn: config.dsn,
environment: config.environment
});
return true;
}
return false;
};
const setUserData: TrackerMethods["setUserData"] = userData =>
Sentry.setUser(userData);
const captureException: TrackerMethods["captureException"] = (e: Error) =>
Sentry.captureException(e);
return {
captureException,
init,
setUserData
};
};

View file

@ -0,0 +1 @@
export { SentryAdapter } from "./Sentry";

View file

@ -0,0 +1,11 @@
import { SentryAdapter } from "./adapters";
import { ErrorTrackerFactory } from "./trackerFactory";
const errorTracker = ErrorTrackerFactory(
SentryAdapter({
dsn: process.env.SENTRY_DSN,
environment: process.env.ENVIRONMENT
})
);
export default errorTracker;

View file

@ -0,0 +1,83 @@
import { ErrorTrackerFactory } from "./trackerFactory";
import { TrackerMethods, TrackerPermission } from "./types";
const testErrorId = "testId";
const initMockFn = jest.fn();
const captureExceptionMockFn = jest.fn(_ => testErrorId);
const setUserDataMockFn = jest.fn();
const TestAdapter = (): TrackerMethods => {
const init: TrackerMethods["init"] = () => {
initMockFn();
return true;
};
const setUserData: TrackerMethods["setUserData"] = userData =>
setUserDataMockFn(userData);
const captureException: TrackerMethods["captureException"] = (e: Error) =>
captureExceptionMockFn(e);
return {
captureException,
init,
setUserData
};
};
describe("Error Tracking", () => {
it("Initiates the tracker", () => {
const errorTracking = ErrorTrackerFactory(TestAdapter());
const enabled = errorTracking.init();
expect(enabled).toBe(true);
expect(initMockFn).toHaveBeenCalled();
});
it("Does not fire events when is not initiated", () => {
const errorTracking = ErrorTrackerFactory(TestAdapter());
const sampleError = new Error("test");
const id = errorTracking.captureException(sampleError);
expect(id).toBe(undefined);
expect(captureExceptionMockFn).toHaveBeenCalledTimes(0);
});
it("Sends a captured exception", () => {
const errorTracking = ErrorTrackerFactory(TestAdapter());
errorTracking.init();
const sampleError = new Error("test");
const id = errorTracking.captureException(sampleError);
expect(id).toBe(testErrorId);
expect(captureExceptionMockFn).toHaveBeenCalledWith(sampleError);
});
it("Does not save user data without permission", () => {
const errorTracking = ErrorTrackerFactory(TestAdapter());
errorTracking.init();
const userData = {
email: "john@example.com",
id: "id",
username: "John Doe"
};
errorTracking.setUserData(userData);
expect(setUserDataMockFn).toHaveBeenCalledTimes(0);
});
it("Does save user data with proper permission", () => {
const errorTracking = ErrorTrackerFactory(TestAdapter(), [
TrackerPermission.USER_DATA
]);
errorTracking.init();
const userData = {
email: "john@example.com",
id: "id",
username: "John Doe"
};
errorTracking.setUserData(userData);
expect(setUserDataMockFn).toHaveBeenCalledWith(userData);
});
});

View file

@ -0,0 +1,52 @@
import { TrackerMethods, TrackerPermission, UserData } from "./types";
type ErrorTrackerFactory = (
ExtensionFactory: TrackerMethods,
permissions?: TrackerPermission[]
) => TrackerMethods;
export const ErrorTrackerFactory: ErrorTrackerFactory = (
extension,
permissions = []
) => {
let ENABLED = false;
const safelyInvoke = <T extends () => any>(
fn: T,
permission?: TrackerPermission
): ReturnType<T> => {
const hasPermission =
permission !== undefined ? permissions.includes(permission) : true;
if (ENABLED && hasPermission) {
try {
return fn();
} catch (e) {
throw new Error(`Tracking Extension Error: ${e}`);
}
}
};
const init: TrackerMethods["init"] = () => {
if (!ENABLED) {
ENABLED = extension.init();
}
return ENABLED;
};
const setUserData: TrackerMethods["setUserData"] = (userData: UserData) =>
safelyInvoke(
() => extension.setUserData(userData),
TrackerPermission.USER_DATA
);
const captureException: TrackerMethods["captureException"] = (e: Error) =>
safelyInvoke(() => extension.captureException(e));
return {
captureException,
init,
setUserData
};
};

View file

@ -0,0 +1,15 @@
export type UserData = {
id: string;
email: string;
username: string;
} | null;
export interface TrackerMethods {
init: () => boolean;
setUserData: (userData: UserData) => void;
captureException: (e: Error) => string | null | undefined;
}
export enum TrackerPermission {
USER_DATA
}

View file

@ -4,6 +4,7 @@ const CheckerPlugin = require("fork-ts-checker-webpack-plugin");
const webpack = require("webpack"); const webpack = require("webpack");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
require("dotenv").config(); require("dotenv").config();
@ -26,7 +27,9 @@ const environmentPlugin = new webpack.EnvironmentPlugin({
API_URI: "", API_URI: "",
APP_MOUNT_URI: "/", APP_MOUNT_URI: "/",
DEMO_MODE: false, DEMO_MODE: false,
GTM_ID: "" ENVIRONMENT: "",
GTM_ID: "",
SENTRY_DSN: ""
}); });
const dashboardBuildPath = "build/dashboard/"; const dashboardBuildPath = "build/dashboard/";
@ -60,6 +63,20 @@ module.exports = (env, argv) => {
fileLoaderPath = "file-loader?name=[name].[ext]"; fileLoaderPath = "file-loader?name=[name].[ext]";
} }
// Create release if sentry config is set
let sentryPlugin;
if (
process.env.SENTRY_ORG &&
process.env.SENTRY_PROJECT &&
process.env.SENTRY_DSN &&
process.env.SENTRY_AUTH_TOKEN
) {
sentryPlugin = new SentryWebpackPlugin({
include: "./build/dashboard/",
urlPrefix: process.env.SENTRY_URL_PREFIX
});
}
return { return {
devServer: { devServer: {
compress: true, compress: true,
@ -68,7 +85,7 @@ module.exports = (env, argv) => {
hot: true, hot: true,
port: 9000 port: 9000
}, },
devtool: "sourceMap", devtool: "source-map",
entry: { entry: {
dashboard: "./src/index.tsx" dashboard: "./src/index.tsx"
}, },
@ -100,7 +117,12 @@ module.exports = (env, argv) => {
splitChunks: false splitChunks: false
}, },
output, output,
plugins: [checkerPlugin, environmentPlugin, htmlWebpackPlugin], plugins: [
checkerPlugin,
environmentPlugin,
htmlWebpackPlugin,
sentryPlugin
].filter(Boolean),
resolve: { resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"], extensions: [".js", ".jsx", ".ts", ".tsx"],
plugins: [pathsPlugin] plugins: [pathsPlugin]