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:
parent
10131163c1
commit
383055a2de
18 changed files with 451 additions and 23 deletions
7
.github/workflows/deploy-staging.yaml
vendored
7
.github/workflows/deploy-staging.yaml
vendored
|
@ -11,6 +11,12 @@ jobs:
|
|||
API_URI: https://master.staging.saleor.cloud/graphql/
|
||||
APP_MOUNT_URI: /dashboard/
|
||||
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:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Package
|
||||
|
@ -41,4 +47,3 @@ jobs:
|
|||
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 cloudfront create-invalidation --distribution-id ${{ secrets.AWS_STAGING_CF_DIST_ID }} --paths "/dashboard*"
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
- Add reference attributes - #917 by @orzechdev
|
||||
- Add product reference attributes - #948 by @orzechdev
|
||||
- Drop descriptionJson and contentJson fields - #950 by @jwm0
|
||||
- Add error tracking with Sentry adapter - #956 by @jwm0
|
||||
|
||||
# 2.11.1
|
||||
|
||||
|
|
41
README.md
41
README.md
|
@ -61,7 +61,7 @@ $ npm i
|
|||
|
||||
### 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.
|
||||
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
|
||||
```
|
||||
|
||||
### 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)
|
||||
|
|
120
package-lock.json
generated
120
package-lock.json
generated
|
@ -3153,6 +3153,120 @@
|
|||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-1.2.0.tgz",
|
||||
|
@ -18614,6 +18728,12 @@
|
|||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"@material-ui/icons": "^4.5.1",
|
||||
"@material-ui/styles": "^4.5.2",
|
||||
"@saleor/macaw-ui": "^0.1.1-9",
|
||||
"@sentry/react": "^6.0.0",
|
||||
"apollo": "^2.21.2",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
|
@ -92,6 +93,7 @@
|
|||
"@pollyjs/adapter-node-http": "^5.0.0",
|
||||
"@pollyjs/core": "^5.0.0",
|
||||
"@pollyjs/persister-fs": "^5.0.0",
|
||||
"@sentry/webpack-plugin": "^1.14.0",
|
||||
"@storybook/addon-storyshots": "^5.2.8",
|
||||
"@storybook/react": "^5.1.9",
|
||||
"@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": "jest src/",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { User } from "@saleor/fragments/types/User";
|
|||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getMutationStatus } from "@saleor/misc";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
import {
|
||||
isSupported as isCredentialsManagementAPISupported,
|
||||
login as loginWithCredentialsManagementAPI,
|
||||
|
@ -52,7 +53,15 @@ export function useAuthProvider(
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userContext && !userContext.isStaff) {
|
||||
if (userContext) {
|
||||
const { id, email, firstName, lastName } = userContext;
|
||||
errorTracker.setUserData({
|
||||
email,
|
||||
id,
|
||||
username: `${firstName} ${lastName}`
|
||||
});
|
||||
|
||||
if (!userContext.isStaff) {
|
||||
logout();
|
||||
notify({
|
||||
status: "error",
|
||||
|
@ -60,6 +69,7 @@ export function useAuthProvider(
|
|||
title: intl.formatMessage(commonMessages.insufficientPermissions)
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [userContext]);
|
||||
|
||||
const logout = () => {
|
||||
|
|
|
@ -239,8 +239,11 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
|||
</div>
|
||||
<main className={classes.view}>
|
||||
{appState.error
|
||||
? appState.error === "unhandled" && (
|
||||
<ErrorPage onBack={handleErrorBack} />
|
||||
? appState.error.type === "unhandled" && (
|
||||
<ErrorPage
|
||||
id={appState.error.id}
|
||||
onBack={handleErrorBack}
|
||||
/>
|
||||
)
|
||||
: children}
|
||||
</main>
|
||||
|
|
|
@ -7,6 +7,7 @@ import SVG from "react-inlinesvg";
|
|||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export interface ErrorPageProps {
|
||||
id?: string | null;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
|
@ -31,6 +32,9 @@ const useStyles = makeStyles(
|
|||
margin: "0 auto",
|
||||
width: 830
|
||||
},
|
||||
errorId: {
|
||||
marginTop: theme.spacing(3)
|
||||
},
|
||||
innerContainer: {
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
order: 1,
|
||||
|
@ -58,7 +62,7 @@ const useStyles = makeStyles(
|
|||
);
|
||||
|
||||
const ErrorPage: React.FC<ErrorPageProps> = props => {
|
||||
const { onBack } = props;
|
||||
const { onBack, id } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
|
||||
|
@ -79,6 +83,11 @@ const ErrorPage: React.FC<ErrorPageProps> = props => {
|
|||
<Typography>
|
||||
<FormattedMessage defaultMessage="Don't worry, everything is gonna be fine" />
|
||||
</Typography>
|
||||
{!!id && (
|
||||
<Typography variant="subtitle2" className={classes.errorId}>
|
||||
Error ID: {id}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
|
|
|
@ -4,16 +4,24 @@ export type AppStateReducerActionType = "displayError" | "displayLoader";
|
|||
|
||||
export interface AppStateReducerAction {
|
||||
payload: Partial<{
|
||||
error: AppError;
|
||||
error: AppError["type"];
|
||||
errorId: AppError["id"];
|
||||
value: boolean;
|
||||
}>;
|
||||
type: AppStateReducerActionType;
|
||||
}
|
||||
|
||||
function displayError(prevState: IAppState, error: AppError): IAppState {
|
||||
function displayError(
|
||||
prevState: IAppState,
|
||||
errorType: AppError["type"],
|
||||
errorId?: AppError["id"]
|
||||
): IAppState {
|
||||
return {
|
||||
...prevState,
|
||||
error,
|
||||
error: {
|
||||
id: errorId,
|
||||
type: errorType
|
||||
},
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
|
@ -31,7 +39,11 @@ function reduceAppState(
|
|||
): IAppState {
|
||||
switch (action.type) {
|
||||
case "displayError":
|
||||
return displayError(prevState, action.payload.error);
|
||||
return displayError(
|
||||
prevState,
|
||||
action.payload.error,
|
||||
action.payload.errorId
|
||||
);
|
||||
case "displayLoader":
|
||||
return displayLoader(prevState, action.payload.value);
|
||||
default:
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
export type AppError = "unhandled";
|
||||
export interface AppError {
|
||||
type: "unhandled";
|
||||
id: string | null | undefined;
|
||||
}
|
||||
|
||||
interface IAppState {
|
||||
error: AppError | null;
|
||||
|
|
|
@ -54,6 +54,7 @@ import PermissionGroupSection from "./permissionGroups";
|
|||
import PluginsSection from "./plugins";
|
||||
import ProductSection from "./products";
|
||||
import ProductTypesSection from "./productTypes";
|
||||
import errorTracker from "./services/errorTracking";
|
||||
import ShippingSection from "./shipping";
|
||||
import SiteSettingsSection from "./siteSettings";
|
||||
import StaffSection from "./staff";
|
||||
|
@ -67,6 +68,8 @@ if (process.env.GTM_ID) {
|
|||
TagManager.initialize({ gtmId: GTM_ID });
|
||||
}
|
||||
|
||||
errorTracker.init();
|
||||
|
||||
// DON'T TOUCH THIS
|
||||
// These are separate clients and do not share configs between themselves
|
||||
// so we need to explicitly set them
|
||||
|
@ -159,14 +162,17 @@ const Routes: React.FC = () => {
|
|||
{homePageLoaded ? (
|
||||
<AppLayout>
|
||||
<ErrorBoundary
|
||||
onError={() =>
|
||||
onError={e => {
|
||||
const errorId = errorTracker.captureException(e);
|
||||
|
||||
dispatchAppState({
|
||||
payload: {
|
||||
errorId,
|
||||
error: "unhandled"
|
||||
},
|
||||
type: "displayError"
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<SectionRoute exact path="/" component={HomePage} />
|
||||
|
|
33
src/services/errorTracking/adapters/Sentry.ts
Normal file
33
src/services/errorTracking/adapters/Sentry.ts
Normal 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
|
||||
};
|
||||
};
|
1
src/services/errorTracking/adapters/index.ts
Normal file
1
src/services/errorTracking/adapters/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { SentryAdapter } from "./Sentry";
|
11
src/services/errorTracking/index.ts
Normal file
11
src/services/errorTracking/index.ts
Normal 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;
|
83
src/services/errorTracking/trackerFactory.test.ts
Normal file
83
src/services/errorTracking/trackerFactory.test.ts
Normal 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);
|
||||
});
|
||||
});
|
52
src/services/errorTracking/trackerFactory.ts
Normal file
52
src/services/errorTracking/trackerFactory.ts
Normal 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
|
||||
};
|
||||
};
|
15
src/services/errorTracking/types.ts
Normal file
15
src/services/errorTracking/types.ts
Normal 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
|
||||
}
|
|
@ -4,6 +4,7 @@ const CheckerPlugin = require("fork-ts-checker-webpack-plugin");
|
|||
const webpack = require("webpack");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
|
@ -26,7 +27,9 @@ const environmentPlugin = new webpack.EnvironmentPlugin({
|
|||
API_URI: "",
|
||||
APP_MOUNT_URI: "/",
|
||||
DEMO_MODE: false,
|
||||
GTM_ID: ""
|
||||
ENVIRONMENT: "",
|
||||
GTM_ID: "",
|
||||
SENTRY_DSN: ""
|
||||
});
|
||||
|
||||
const dashboardBuildPath = "build/dashboard/";
|
||||
|
@ -60,6 +63,20 @@ module.exports = (env, argv) => {
|
|||
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 {
|
||||
devServer: {
|
||||
compress: true,
|
||||
|
@ -68,7 +85,7 @@ module.exports = (env, argv) => {
|
|||
hot: true,
|
||||
port: 9000
|
||||
},
|
||||
devtool: "sourceMap",
|
||||
devtool: "source-map",
|
||||
entry: {
|
||||
dashboard: "./src/index.tsx"
|
||||
},
|
||||
|
@ -100,7 +117,12 @@ module.exports = (env, argv) => {
|
|||
splitChunks: false
|
||||
},
|
||||
output,
|
||||
plugins: [checkerPlugin, environmentPlugin, htmlWebpackPlugin],
|
||||
plugins: [
|
||||
checkerPlugin,
|
||||
environmentPlugin,
|
||||
htmlWebpackPlugin,
|
||||
sentryPlugin
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||
plugins: [pathsPlugin]
|
||||
|
|
Loading…
Reference in a new issue