@exodus/error-tracking 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [3.0.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/error-tracking@2.2.0...@exodus/error-tracking@3.0.0) (2025-07-01)
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ - merge errorTracking.track/trackRemote, enable/disable remote tracking based on config (#13038)
11
+
12
+ ### Features
13
+
14
+ - feat!: merge errorTracking.track/trackRemote, enable/disable remote tracking based on config (#13038)
15
+
6
16
  ## [2.2.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/error-tracking@2.1.0...@exodus/error-tracking@2.2.0) (2025-06-23)
7
17
 
8
18
  ### Features
package/api/index.d.ts CHANGED
@@ -11,16 +11,6 @@ export interface ErrorTrackingApi {
11
11
  * ```
12
12
  */
13
13
  track(params: { error: Error; namespace: string; context?: any }): Promise<void>
14
- /**
15
- * Track an error remotely using sentry if available
16
- * @example
17
- * ```typescript
18
- * exodus.errors.trackRemote({
19
- * error: 'Encountered an issue when computing total balances',
20
- * })
21
- * ```
22
- */
23
- trackRemote(params: { error: string }): Promise<void>
24
14
  }
25
15
 
26
16
  declare const errorTrackingApiDefinition: {
package/api/index.js CHANGED
@@ -1,33 +1,10 @@
1
- const createTrackRemote = ({ remoteErrorTracking, logger }) => {
2
- if (!remoteErrorTracking) {
3
- return async ({ error }) => {
4
- logger.debug('remote error tracking is disabled', error)
5
- }
6
- }
7
-
8
- return async ({ error }) => {
9
- try {
10
- await remoteErrorTracking.captureError({
11
- error,
12
- })
13
- } catch (err) {
14
- logger.error('failed to remote track error', err)
15
- }
16
- }
17
- }
18
-
19
1
  export const errorTrackingApiDefinition = {
20
2
  id: 'errorTrackingApi',
21
3
  type: 'api',
22
- factory: ({ errorTracking, logger, remoteErrorTracking }) => {
23
- const trackRemote = createTrackRemote({ remoteErrorTracking, logger })
24
-
25
- return {
26
- errors: {
27
- track: errorTracking.track,
28
- trackRemote,
29
- },
30
- }
31
- },
32
- dependencies: ['logger', 'remoteErrorTracking?', 'errorTracking'],
4
+ factory: ({ errorTracking }) => ({
5
+ errors: {
6
+ track: errorTracking.track,
7
+ },
8
+ }),
9
+ dependencies: ['errorTracking'],
33
10
  }
package/atoms/index.js CHANGED
@@ -5,11 +5,16 @@ import { createInMemoryAtom } from '@exodus/atoms'
5
5
  errors: [{ `namespace`, `error`, `context`, `time` }, ...]
6
6
  }
7
7
  */
8
- const errorsAtomDefinition = {
8
+ export const errorsAtomDefinition = {
9
9
  id: 'errorsAtom',
10
10
  type: 'atom',
11
11
  factory: () => createInMemoryAtom({ defaultValue: { errors: [] } }),
12
12
  dependencies: [],
13
13
  }
14
14
 
15
- export default errorsAtomDefinition
15
+ export const remoteErrorTrackingEnabledAtomDefinition = {
16
+ id: 'remoteErrorTrackingEnabledAtom',
17
+ type: 'atom',
18
+ // eslint-disable-next-line @exodus/hydra/in-memory-atom-default-value
19
+ factory: () => createInMemoryAtom(),
20
+ }
package/index.js CHANGED
@@ -1,28 +1,47 @@
1
1
  import typeforce from '@exodus/typeforce'
2
2
 
3
- import { isEmpty } from './is-empty.js'
4
3
  import { errorTrackingApiDefinition } from './api/index.js'
5
- import errorTrackingAtomDefinition from './atoms/index.js'
4
+ import { errorsAtomDefinition, remoteErrorTrackingEnabledAtomDefinition } from './atoms/index.js'
6
5
  import errorTrackingReportDefinition from './report/index.js'
7
6
  import { errorTrackingDefinition, remoteErrorTrackingDefinition } from './module/index.js'
7
+ import errorTrackingPluginDefinition from './plugin/index.js'
8
8
 
9
- const defaultConfig = { maxErrorsCount: 100 }
9
+ const defaultConfig = {
10
+ maxErrorsCount: 100,
11
+ sentryConfig: undefined,
12
+ remoteErrorTrackingABExperimentId: 'sentry',
13
+ trackWalletsCreatedAfter: new Date('2025-07-21'),
14
+ trackFundedWallets: false,
15
+ }
10
16
 
11
17
  const configSchema = {
12
18
  maxErrorsCount: (value) => typeof value === 'number' && value > 0 && value <= 9999,
13
19
  sentryConfig: '?Object',
20
+ remoteErrorTrackingABExperimentId: '?String',
21
+ trackWalletsCreatedAfter: '?Date',
22
+ trackFundedWallets: '?Boolean',
14
23
  }
15
24
 
16
25
  const errorTracking = (config = Object.create(null)) => {
17
26
  config = { ...defaultConfig, ...config }
18
27
 
19
- const { maxErrorsCount, sentryConfig } = typeforce.parse(configSchema, config)
28
+ const {
29
+ maxErrorsCount,
30
+ sentryConfig,
31
+ remoteErrorTrackingABExperimentId,
32
+ trackWalletsCreatedAfter,
33
+ trackFundedWallets,
34
+ } = typeforce.parse(configSchema, config, true)
20
35
 
36
+ const remoteErrorTrackingAvailable = !!sentryConfig && !!remoteErrorTrackingABExperimentId
21
37
  return {
22
38
  id: 'errorTracking',
23
39
  definitions: [
24
40
  {
25
- definition: errorTrackingAtomDefinition,
41
+ definition: errorsAtomDefinition,
42
+ },
43
+ {
44
+ definition: remoteErrorTrackingEnabledAtomDefinition,
26
45
  },
27
46
  {
28
47
  definition: errorTrackingApiDefinition,
@@ -32,13 +51,22 @@ const errorTracking = (config = Object.create(null)) => {
32
51
  config: { maxErrorsCount },
33
52
  },
34
53
  {
35
- if: sentryConfig !== undefined && sentryConfig !== null && !isEmpty(sentryConfig),
54
+ if: remoteErrorTrackingAvailable,
36
55
  definition: remoteErrorTrackingDefinition,
37
56
  config: sentryConfig,
38
57
  },
58
+ // deprecated, errors will go to sentry
39
59
  {
40
60
  definition: errorTrackingReportDefinition,
41
61
  },
62
+ {
63
+ definition: errorTrackingPluginDefinition,
64
+ config: {
65
+ remoteErrorTrackingABExperimentId,
66
+ trackWalletsCreatedAfter,
67
+ trackFundedWallets,
68
+ },
69
+ },
42
70
  ],
43
71
  }
44
72
  }
@@ -1,16 +1,25 @@
1
1
  const MODULE_ID = 'errorTracking'
2
2
 
3
- const createErrorTracking = ({ errorsAtom, config }) => {
3
+ const createErrorTracking = ({
4
+ errorsAtom,
5
+ remoteErrorTrackingEnabledAtom,
6
+ remoteErrorTracking,
7
+ config,
8
+ logger,
9
+ }) => {
4
10
  const track = async ({ error, context, namespace }) => {
5
- if (!namespace) {
6
- throw new Error('no namespace provided')
11
+ if (namespace !== undefined && typeof namespace !== 'string') {
12
+ throw new Error('namespace must be a string')
7
13
  }
8
14
 
9
15
  if (!(error instanceof Error)) {
10
16
  throw new TypeError('error must be an instance of Error')
11
17
  }
12
18
 
13
- return errorsAtom.set(({ errors }) => {
19
+ // TODO: figure out what to do with `context`
20
+
21
+ // eventually kill this and only track remote
22
+ await errorsAtom.set(({ errors }) => {
14
23
  return {
15
24
  // this array can be big. not sure about prefering spread operator here
16
25
  // concat function seems like a better option
@@ -20,6 +29,15 @@ const createErrorTracking = ({ errorsAtom, config }) => {
20
29
  .slice(0, config.maxErrorsCount),
21
30
  }
22
31
  })
32
+
33
+ if (remoteErrorTracking) {
34
+ remoteErrorTrackingEnabledAtom
35
+ .get()
36
+ .then((enabled) => enabled && remoteErrorTracking.track({ error }))
37
+ .catch((err) => {
38
+ logger.error('failed to upload error', error, err)
39
+ })
40
+ }
23
41
  }
24
42
 
25
43
  return { track }
@@ -29,6 +47,12 @@ export const errorTrackingDefinition = {
29
47
  id: MODULE_ID,
30
48
  type: 'module',
31
49
  factory: createErrorTracking,
32
- dependencies: ['config', 'errorsAtom'],
50
+ dependencies: [
51
+ 'config',
52
+ 'errorsAtom',
53
+ 'remoteErrorTrackingEnabledAtom',
54
+ 'remoteErrorTracking?',
55
+ 'logger',
56
+ ],
33
57
  public: true,
34
58
  }
@@ -6,10 +6,14 @@ export const remoteErrorTrackingDefinition = {
6
6
  type: 'module',
7
7
  factory: ({ config, fetch }) => {
8
8
  const fetchival = createFetchival({ fetch })
9
- return createSentryClient({
9
+ const client = createSentryClient({
10
10
  config,
11
11
  fetchival,
12
12
  })
13
+
14
+ return {
15
+ track: ({ error }) => client.captureError({ error }),
16
+ }
13
17
  },
14
18
  dependencies: ['config', 'fetch'],
15
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/error-tracking",
3
- "version": "2.2.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "description": "A simple error tracking package to let any feature collect errors and create the report",
6
6
  "author": "Exodus Movement, Inc.",
@@ -16,14 +16,14 @@
16
16
  "main": "index.js",
17
17
  "exports": "./index.js",
18
18
  "files": [
19
- "index.d.ts",
20
- "is-empty.js",
21
19
  "api",
22
20
  "atoms",
23
21
  "module",
22
+ "plugin",
24
23
  "report",
25
24
  "CHANGELOG.md",
26
25
  "README.md",
26
+ "index.d.ts",
27
27
  "!**/__tests__/**"
28
28
  ],
29
29
  "scripts": {
@@ -43,5 +43,5 @@
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
46
- "gitHead": "529b727a65d95366a9b7dd08b2e7acb6411b0de2"
46
+ "gitHead": "7425cf446dbe32c407ac46bd1c0463787cf8deb6"
47
47
  }
@@ -0,0 +1,82 @@
1
+ import { combine, compute } from '@exodus/atoms'
2
+
3
+ import whyIsRemoteTrackingDisabled from './why-is-remote-tracking-disabled.js'
4
+
5
+ function errorTrackingPlugin({
6
+ abTestingAtom,
7
+ earliestTxDateAtom,
8
+ walletCreatedAtAtom,
9
+ getBuildMetadata,
10
+ remoteErrorTrackingEnabledAtom,
11
+ config: { remoteErrorTrackingABExperimentId, trackWalletsCreatedAfter, trackFundedWallets },
12
+ logger,
13
+ }) {
14
+ const subscriptions = []
15
+
16
+ const onAssetsSynced = async () => {
17
+ if (!abTestingAtom) {
18
+ logger.debug('remote error tracking is disabled, ab-testing feature not found')
19
+ return
20
+ }
21
+
22
+ // defined locally instead of in /atoms to avoid circular dependency
23
+ // literally everything depends on error-tracking, so error-tracking atoms can't depend on other features
24
+ const internalRemoteErrorTrackingEnabledAtom = compute({
25
+ atom: combine({
26
+ abTesting: abTestingAtom,
27
+ earliestTxDate: earliestTxDateAtom,
28
+ walletCreatedAt: walletCreatedAtAtom,
29
+ }),
30
+ selector: async ({ abTesting, earliestTxDate, walletCreatedAt }) => {
31
+ const reasonDisabled = whyIsRemoteTrackingDisabled({
32
+ remoteErrorTrackingABExperimentId,
33
+ abTesting,
34
+ trackWalletsCreatedAfter,
35
+ walletCreatedAt,
36
+ trackFundedWallets,
37
+ earliestTxDate,
38
+ buildMetadata: await getBuildMetadata(),
39
+ })
40
+
41
+ if (reasonDisabled) {
42
+ logger.debug(`remote error tracking is disabled: ${reasonDisabled}`)
43
+ return false
44
+ }
45
+
46
+ logger.debug('remote error tracking is enabled')
47
+ return true
48
+ },
49
+ })
50
+
51
+ subscriptions.push(
52
+ internalRemoteErrorTrackingEnabledAtom.observe(remoteErrorTrackingEnabledAtom.set)
53
+ )
54
+ }
55
+
56
+ const onStop = () => {
57
+ subscriptions.forEach((unsubscribe) => unsubscribe())
58
+ subscriptions.length = 0
59
+ }
60
+
61
+ return {
62
+ onAssetsSynced,
63
+ onStop,
64
+ }
65
+ }
66
+
67
+ const errorTrackingPluginDefinition = {
68
+ id: 'errorTrackingPlugin',
69
+ type: 'plugin',
70
+ factory: errorTrackingPlugin,
71
+ dependencies: [
72
+ 'abTestingAtom?',
73
+ 'earliestTxDateAtom',
74
+ 'walletCreatedAtAtom',
75
+ 'remoteErrorTrackingEnabledAtom',
76
+ 'getBuildMetadata',
77
+ 'config',
78
+ 'logger',
79
+ ],
80
+ }
81
+
82
+ export default errorTrackingPluginDefinition
@@ -0,0 +1,23 @@
1
+ export default function whyIsRemoteTrackingDisabled({
2
+ remoteErrorTrackingABExperimentId,
3
+ abTesting,
4
+ trackWalletsCreatedAfter,
5
+ walletCreatedAt,
6
+ trackFundedWallets,
7
+ earliestTxDate,
8
+ buildMetadata,
9
+ }) {
10
+ if (!remoteErrorTrackingABExperimentId) return 'missing-ab-experiment-id'
11
+ if (!abTesting.experiments?.[remoteErrorTrackingABExperimentId]?.enabled)
12
+ return 'ab-experiment-disabled'
13
+ if (
14
+ trackWalletsCreatedAfter &&
15
+ // the negation's a bit harder to read but is a safer check
16
+ !(new Date(walletCreatedAt) > trackWalletsCreatedAfter)
17
+ ) {
18
+ return 'wallet-too-old'
19
+ }
20
+
21
+ if (!trackFundedWallets && earliestTxDate) return 'not-tracking-funded-wallets'
22
+ if (buildMetadata.dev) return 'dev-mode'
23
+ }
package/is-empty.js DELETED
@@ -1,9 +0,0 @@
1
- export const isEmpty = (obj) => {
2
- for (const prop in obj) {
3
- if (Object.hasOwn(obj, prop)) {
4
- return false
5
- }
6
- }
7
-
8
- return true
9
- }