Skip to content

Commit 4917804

Browse files
authored
build: Check for circular deps with madge (getsentry#3631)
* build: Check for circular dep with madge Madge (https://www.npmjs.com/package/madge) is a library that can traverse module dependency trees to check for circular dependencies. We can leverage it using `madge --circular /path/to/index.ts` to make sure that our packages don't contain circular deps. On average madge takes around 2-3 sec, so we are not sacrificing a ton of CI time. * fix(node): Remove circular dependency To remove the circular relationship between backend and parsers, we extract the NodeOptions type into a separate types.ts file. * fix(hub): Remove circular dep between hub and interface To remove this, the interface.ts was removed and all of it's types were moved into hub.ts * fix(hub): Remove circular dep from session To remove this circular dep, we extract the SessionFlusher from session.ts into it's own file. * fix(nextjs): Remove circular dependency To remove the circular dependency between index.server.ts and utils/instrumentServer.ts, we rely on importing Sentry func (like startTransaction and captureException) from `@sentry/node` directly. * build(types): Don't run madge on @sentry/types * build: Run circularDepCheck github action
1 parent 997a782 commit 4917804

35 files changed

+674
-243
lines changed

.github/workflows/build.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,29 @@ jobs:
139139
- name: Run linter
140140
run: yarn lint
141141

142+
job_circular_dep_check:
143+
name: Circular Dependency Check
144+
needs: job_build
145+
timeout-minutes: 10
146+
runs-on: ubuntu-latest
147+
steps:
148+
- name: Check out current commit (${{ github.sha }})
149+
uses: actions/checkout@v2
150+
- name: Set up Node
151+
uses: actions/setup-node@v1
152+
- name: Check dependency cache
153+
uses: actions/cache@v2
154+
with:
155+
path: ${{ env.CACHED_DEPENDENCY_PATHS }}
156+
key: ${{ needs.job_build.outputs.dependency_cache_key }}
157+
- name: Check build cache
158+
uses: actions/cache@v2
159+
with:
160+
path: ${{ env.CACHED_BUILD_PATHS }}
161+
key: ${{ env.BUILD_CACHE_KEY }}
162+
- name: Run madge
163+
run: yarn circularDepCheck
164+
142165
job_unit_test:
143166
name: Test (Node ${{ matrix.node }})
144167
needs: job_build

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"codecov": "codecov",
1515
"pack:changed": "lerna run pack --since",
1616
"prepublishOnly": "lerna run --stream --concurrency 1 prepublishOnly",
17-
"postpublish": "make publish-docs && lerna run --stream --concurrency 1 postpublish"
17+
"postpublish": "make publish-docs && lerna run --stream --concurrency 1 postpublish",
18+
"circularDepCheck": "lerna run --parallel circularDepCheck"
1819
},
1920
"volta": {
2021
"node": "14.17.0",
@@ -58,6 +59,7 @@
5859
"karma-browserstack-launcher": "^1.5.1",
5960
"karma-firefox-launcher": "^1.1.0",
6061
"lerna": "3.13.4",
62+
"madge": "4.0.2",
6163
"mocha": "^6.1.4",
6264
"npm-run-all": "^4.1.2",
6365
"prettier": "1.19.0",

packages/angular/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"fix": "run-s fix:eslint fix:prettier",
5353
"fix:prettier": "prettier --write \"{src,test}/**/*.ts\"",
5454
"fix:eslint": "eslint . --format stylish --fix",
55-
"pack": "npm pack"
55+
"pack": "npm pack",
56+
"circularDepCheck": "madge --circular src/index.ts"
5657
},
5758
"volta": {
5859
"extends": "../../package.json"

packages/browser/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@
8282
"size:check": "run-p size:check:es5 size:check:es6",
8383
"size:check:es5": "cat build/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES5: \",$1,\"kB\";}'",
8484
"size:check:es6": "cat build/bundle.es6.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES6: \",$1,\"kB\";}'",
85-
"pack": "npm pack"
85+
"pack": "npm pack",
86+
"circularDepCheck": "madge --circular src/index.ts"
8687
},
8788
"volta": {
8889
"extends": "../../package.json"

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
"test": "jest",
4949
"test:watch": "jest --watch",
5050
"pack": "npm pack",
51-
"version": "node ../../scripts/versionbump.js src/version.ts"
51+
"version": "node ../../scripts/versionbump.js src/version.ts",
52+
"circularDepCheck": "madge --circular src/index.ts"
5253
},
5354
"volta": {
5455
"extends": "../../package.json"

packages/eslint-config-sdk/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"link:yarn": "yarn link",
4141
"lint": "prettier --check \"**/*.js\"",
4242
"fix": "prettier --write \"**/*.js\"",
43-
"pack": "npm pack"
43+
"pack": "npm pack",
44+
"circularDepCheck": "madge --circular src/index.js"
4445
},
4546
"volta": {
4647
"extends": "../../package.json"

packages/eslint-plugin-sdk/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"lint": "prettier --check \"{src,test}/**/*.js\"",
3232
"fix": "prettier --write \"{src,test}/**/*.js\"",
3333
"test": "mocha test --recursive",
34-
"pack": "npm pack"
34+
"pack": "npm pack",
35+
"circularDepCheck": "madge --circular src/index.js"
3536
},
3637
"volta": {
3738
"extends": "../../package.json"

packages/gatsby/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"fix:eslint": "eslint . --format stylish --fix",
6060
"test": "jest",
6161
"test:watch": "jest --watch",
62-
"pack": "npm pack"
62+
"pack": "npm pack",
63+
"circularDepCheck": "madge --circular src/index.ts"
6364
},
6465
"volta": {
6566
"extends": "../../package.json"

packages/hub/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"fix:eslint": "eslint . --format stylish --fix",
4646
"test": "jest",
4747
"test:watch": "jest --watch",
48-
"pack": "npm pack"
48+
"pack": "npm pack",
49+
"circularDepCheck": "madge --circular src/index.ts"
4950
},
5051
"volta": {
5152
"extends": "../../package.json"

packages/hub/src/hub.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
} from '@sentry/types';
2424
import { consoleSandbox, dateTimestampInSeconds, getGlobalObject, isNodeEnv, logger, uuid4 } from '@sentry/utils';
2525

26-
import { Carrier, DomainAsCarrier, Layer } from './interfaces';
2726
import { Scope } from './scope';
2827
import { Session } from './session';
2928

@@ -49,6 +48,47 @@ const DEFAULT_BREADCRUMBS = 100;
4948
*/
5049
const MAX_BREADCRUMBS = 100;
5150

51+
/**
52+
* A layer in the process stack.
53+
* @hidden
54+
*/
55+
export interface Layer {
56+
client?: Client;
57+
scope?: Scope;
58+
}
59+
60+
/**
61+
* An object that contains a hub and maintains a scope stack.
62+
* @hidden
63+
*/
64+
export interface Carrier {
65+
__SENTRY__?: {
66+
hub?: Hub;
67+
/**
68+
* Extra Hub properties injected by various SDKs
69+
*/
70+
integrations?: Integration[];
71+
extensions?: {
72+
/** Hack to prevent bundlers from breaking our usage of the domain package in the cross-platform Hub package */
73+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
74+
domain?: { [key: string]: any };
75+
} & {
76+
/** Extension methods for the hub, which are bound to the current Hub instance */
77+
// eslint-disable-next-line @typescript-eslint/ban-types
78+
[key: string]: Function;
79+
};
80+
};
81+
}
82+
83+
/**
84+
* @hidden
85+
* @deprecated Can be removed once `Hub.getActiveDomain` is removed.
86+
*/
87+
export interface DomainAsCarrier extends Carrier {
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89+
members: { [key: string]: any }[];
90+
}
91+
5292
/**
5393
* @inheritDoc
5494
*/

packages/hub/src/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
// eslint-disable-next-line deprecation/deprecation
2-
export { Carrier, DomainAsCarrier, Layer } from './interfaces';
31
export { addGlobalEventProcessor, Scope } from './scope';
4-
export { Session, SessionFlusher } from './session';
2+
export { Session } from './session';
3+
export { SessionFlusher } from './sessionFlusher';
54
export {
65
// eslint-disable-next-line deprecation/deprecation
76
getActiveDomain,
@@ -11,4 +10,8 @@ export {
1110
Hub,
1211
makeMain,
1312
setHubOnCarrier,
13+
Carrier,
14+
// eslint-disable-next-line deprecation/deprecation
15+
DomainAsCarrier,
16+
Layer,
1417
} from './hub';

packages/hub/src/interfaces.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

packages/hub/src/scope.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,11 +450,11 @@ export class Scope implements ScopeInterface {
450450
} else {
451451
const result = processor({ ...event }, hint) as Event | null;
452452
if (isThenable(result)) {
453-
(result as PromiseLike<Event | null>)
453+
void (result as PromiseLike<Event | null>)
454454
.then(final => this._notifyEventProcessors(processors, final, hint, index + 1).then(resolve))
455455
.then(null, reject);
456456
} else {
457-
this._notifyEventProcessors(processors, result, hint, index + 1)
457+
void this._notifyEventProcessors(processors, result, hint, index + 1)
458458
.then(resolve)
459459
.then(null, reject);
460460
}

packages/hub/src/session.ts

Lines changed: 2 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
1-
import {
2-
AggregationCounts,
3-
RequestSessionStatus,
4-
Session as SessionInterface,
5-
SessionAggregates,
6-
SessionContext,
7-
SessionFlusherLike,
8-
SessionStatus,
9-
Transport,
10-
} from '@sentry/types';
11-
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
12-
13-
import { getCurrentHub } from './hub';
1+
import { Session as SessionInterface, SessionContext, SessionStatus } from '@sentry/types';
2+
import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils';
143

154
/**
165
* @inheritdoc
@@ -145,119 +134,3 @@ export class Session implements SessionInterface {
145134
});
146135
}
147136
}
148-
149-
type ReleaseHealthAttributes = {
150-
environment?: string;
151-
release: string;
152-
};
153-
154-
/**
155-
* @inheritdoc
156-
*/
157-
export class SessionFlusher implements SessionFlusherLike {
158-
public readonly flushTimeout: number = 60;
159-
private _pendingAggregates: Record<number, AggregationCounts> = {};
160-
private _sessionAttrs: ReleaseHealthAttributes;
161-
private _intervalId: ReturnType<typeof setInterval>;
162-
private _isEnabled: boolean = true;
163-
private _transport: Transport;
164-
165-
public constructor(transport: Transport, attrs: ReleaseHealthAttributes) {
166-
this._transport = transport;
167-
// Call to setInterval, so that flush is called every 60 seconds
168-
this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000);
169-
this._sessionAttrs = attrs;
170-
}
171-
172-
/** Sends session aggregates to Transport */
173-
public sendSessionAggregates(sessionAggregates: SessionAggregates): void {
174-
if (!this._transport.sendSession) {
175-
logger.warn("Dropping session because custom transport doesn't implement sendSession");
176-
return;
177-
}
178-
void this._transport.sendSession(sessionAggregates).then(null, reason => {
179-
logger.error(`Error while sending session: ${reason}`);
180-
});
181-
}
182-
183-
/** Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSessions` */
184-
public flush(): void {
185-
const sessionAggregates = this.getSessionAggregates();
186-
if (sessionAggregates.aggregates.length === 0) {
187-
return;
188-
}
189-
this._pendingAggregates = {};
190-
this.sendSessionAggregates(sessionAggregates);
191-
}
192-
193-
/** Massages the entries in `pendingAggregates` and returns aggregated sessions */
194-
public getSessionAggregates(): SessionAggregates {
195-
const aggregates: AggregationCounts[] = Object.keys(this._pendingAggregates).map((key: string) => {
196-
return this._pendingAggregates[parseInt(key)];
197-
});
198-
199-
const sessionAggregates: SessionAggregates = {
200-
attrs: this._sessionAttrs,
201-
aggregates,
202-
};
203-
return dropUndefinedKeys(sessionAggregates);
204-
}
205-
206-
/** JSDoc */
207-
public close(): void {
208-
clearInterval(this._intervalId);
209-
this._isEnabled = false;
210-
this.flush();
211-
}
212-
213-
/**
214-
* Wrapper function for _incrementSessionStatusCount that checks if the instance of SessionFlusher is enabled then
215-
* fetches the session status of the request from `Scope.getRequestSession().status` on the scope and passes them to
216-
* `_incrementSessionStatusCount` along with the start date
217-
*/
218-
public incrementSessionStatusCount(): void {
219-
if (!this._isEnabled) {
220-
return;
221-
}
222-
const scope = getCurrentHub().getScope();
223-
const requestSession = scope?.getRequestSession();
224-
225-
if (requestSession && requestSession.status) {
226-
this._incrementSessionStatusCount(requestSession.status, new Date());
227-
// This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in
228-
// case captureRequestSession is called more than once to prevent double count
229-
scope?.setRequestSession(undefined);
230-
231-
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
232-
}
233-
}
234-
235-
/**
236-
* Increments status bucket in pendingAggregates buffer (internal state) corresponding to status of
237-
* the session received
238-
*/
239-
private _incrementSessionStatusCount(status: RequestSessionStatus, date: Date): number {
240-
// Truncate minutes and seconds on Session Started attribute to have one minute bucket keys
241-
const sessionStartedTrunc = new Date(date).setSeconds(0, 0);
242-
this._pendingAggregates[sessionStartedTrunc] = this._pendingAggregates[sessionStartedTrunc] || {};
243-
244-
// corresponds to aggregated sessions in one specific minute bucket
245-
// for example, {"started":"2021-03-16T08:00:00.000Z","exited":4, "errored": 1}
246-
const aggregationCounts: AggregationCounts = this._pendingAggregates[sessionStartedTrunc];
247-
if (!aggregationCounts.started) {
248-
aggregationCounts.started = new Date(sessionStartedTrunc).toISOString();
249-
}
250-
251-
switch (status) {
252-
case RequestSessionStatus.Errored:
253-
aggregationCounts.errored = (aggregationCounts.errored || 0) + 1;
254-
return aggregationCounts.errored;
255-
case RequestSessionStatus.Ok:
256-
aggregationCounts.exited = (aggregationCounts.exited || 0) + 1;
257-
return aggregationCounts.exited;
258-
case RequestSessionStatus.Crashed:
259-
aggregationCounts.crashed = (aggregationCounts.crashed || 0) + 1;
260-
return aggregationCounts.crashed;
261-
}
262-
}
263-
}

0 commit comments

Comments
 (0)