Skip to content

Commit 4e68f8f

Browse files
committed
feat: TransactionActivity Integration
1 parent 0dfcdc9 commit 4e68f8f

File tree

2 files changed

+213
-0
lines changed

2 files changed

+213
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { EventProcessor, Hub, Integration, Scope, Span, SpanContext } from '@sentry/types';
2+
3+
/** JSDoc */
4+
interface TransactionActivityOptions {
5+
// onLocationChange: (info) => {
6+
// // info holds the location change api.
7+
// // if this returns `null` there is no transaction started.
8+
// return info.state.transaction || info.url;
9+
// },
10+
// onActivity?: (info) => {
11+
// return info.type !== 'xhr' || !info.url.match(/zendesk/);
12+
// },
13+
idleTimeout?: number;
14+
}
15+
16+
/** JSDoc */
17+
interface Activity {
18+
name: string;
19+
span?: Span;
20+
}
21+
22+
/** JSDoc */
23+
export class TransactionActivity implements Integration {
24+
/**
25+
* @inheritDoc
26+
*/
27+
public name: string = TransactionActivity.id;
28+
29+
/**
30+
* @inheritDoc
31+
*/
32+
public static id: string = 'TransactionActivity';
33+
34+
/** JSDoc */
35+
private static _options: TransactionActivityOptions;
36+
37+
/**
38+
* Returns current hub.
39+
*/
40+
private static _getCurrentHub?: () => Hub;
41+
42+
private static _activeTransaction?: Span;
43+
44+
private static _currentIndex: number = 0;
45+
46+
private static readonly _activities: { [key: number]: Activity } = {};
47+
48+
private static _debounce: number = 0;
49+
50+
/**
51+
* @inheritDoc
52+
*/
53+
public constructor(options?: TransactionActivityOptions) {
54+
TransactionActivity._options = {
55+
idleTimeout: 500,
56+
...options,
57+
};
58+
}
59+
60+
/**
61+
* @inheritDoc
62+
*/
63+
public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
64+
TransactionActivity._getCurrentHub = getCurrentHub;
65+
}
66+
67+
/**
68+
* Internal run loop that checks if activy is running
69+
*/
70+
private static _watchActivity(): void {
71+
const count = Object.keys(TransactionActivity._activities).length;
72+
if (count > 0) {
73+
clearTimeout(TransactionActivity._debounce);
74+
setTimeout(() => {
75+
TransactionActivity._watchActivity();
76+
}, 10);
77+
} else {
78+
TransactionActivity._debounce = (setTimeout(() => {
79+
const active = TransactionActivity._activeTransaction;
80+
if (active) {
81+
active.finish();
82+
}
83+
}, (TransactionActivity._options && TransactionActivity._options.idleTimeout) || 500) as any) as number; // TODO 500
84+
}
85+
}
86+
87+
/**
88+
* Starts a Transaction waiting for activity idle to finish
89+
*/
90+
public static startIdleTransaction(name: string, spanContext?: SpanContext): Span | undefined {
91+
const _getCurrentHub = TransactionActivity._getCurrentHub;
92+
if (!_getCurrentHub) {
93+
return undefined;
94+
}
95+
96+
const hub = _getCurrentHub();
97+
if (!hub) {
98+
return undefined;
99+
}
100+
101+
const span = hub.startSpan({
102+
...spanContext,
103+
transaction: name,
104+
});
105+
106+
TransactionActivity._activeTransaction = span;
107+
108+
hub.configureScope((scope: Scope) => {
109+
scope.setSpan(span);
110+
});
111+
112+
return span;
113+
}
114+
115+
/**
116+
* Starts tracking for a specifc activity
117+
*/
118+
public static pushActivity(name: string, spanContext?: SpanContext): number {
119+
const _getCurrentHub = TransactionActivity._getCurrentHub;
120+
if (spanContext && _getCurrentHub) {
121+
const hub = _getCurrentHub();
122+
if (hub) {
123+
TransactionActivity._activities[TransactionActivity._currentIndex] = {
124+
name,
125+
span: hub.startSpan(spanContext),
126+
};
127+
}
128+
} else {
129+
TransactionActivity._activities[TransactionActivity._currentIndex] = {
130+
name,
131+
};
132+
}
133+
134+
TransactionActivity._watchActivity();
135+
return TransactionActivity._currentIndex++;
136+
}
137+
138+
/**
139+
* Removes activity and finishes the span in case there is one
140+
*/
141+
public static popActivity(id: number): void {
142+
const activity = TransactionActivity._activities[id];
143+
if (activity) {
144+
if (activity.span) {
145+
activity.span.finish();
146+
}
147+
// tslint:disable-next-line: no-dynamic-delete
148+
delete TransactionActivity._activities[id];
149+
}
150+
}
151+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { TransactionActivity } from '../src/transactionactivity';
2+
3+
const transactionActivity: TransactionActivity = new TransactionActivity();
4+
5+
const configureScope: any = jest.fn();
6+
const startSpan: any = jest.fn();
7+
8+
const getCurrentHubMock: any = () => ({
9+
configureScope,
10+
startSpan,
11+
});
12+
13+
transactionActivity.setupOnce(jest.fn(), getCurrentHubMock);
14+
15+
describe('TransactionActivity', () => {
16+
afterEach(() => {
17+
jest.resetAllMocks();
18+
(TransactionActivity as any)._activities = {};
19+
(TransactionActivity as any)._currentIndex = 0;
20+
(TransactionActivity as any)._activeTransaction = undefined;
21+
});
22+
23+
test('startSpan with transaction', () => {
24+
TransactionActivity.startIdleTransaction('test');
25+
expect(startSpan).toBeCalled();
26+
expect(startSpan.mock.calls[0][0].transaction).toBe('test');
27+
});
28+
29+
test('track activity', () => {
30+
jest.useFakeTimers();
31+
const spy = jest.spyOn(TransactionActivity as any, '_watchActivity');
32+
33+
TransactionActivity.pushActivity('xhr');
34+
expect(spy).toBeCalledTimes(1);
35+
jest.runOnlyPendingTimers();
36+
expect(spy).toBeCalledTimes(2);
37+
jest.runOnlyPendingTimers();
38+
expect(spy).toBeCalledTimes(3);
39+
});
40+
41+
test('multiple activities ', () => {
42+
TransactionActivity.pushActivity('xhr');
43+
const a = TransactionActivity.pushActivity('xhr2');
44+
TransactionActivity.popActivity(a);
45+
TransactionActivity.pushActivity('xhr3');
46+
expect(Object.keys((TransactionActivity as any)._activities)).toHaveLength(2);
47+
});
48+
49+
test.only('finishing a transaction after debounce', () => {
50+
jest.useFakeTimers();
51+
const spy = jest.spyOn(TransactionActivity as any, '_watchActivity');
52+
TransactionActivity.startIdleTransaction('test');
53+
const a = TransactionActivity.pushActivity('xhr');
54+
expect(spy).toBeCalledTimes(1);
55+
expect(Object.keys((TransactionActivity as any)._activities)).toHaveLength(1);
56+
TransactionActivity.popActivity(a);
57+
expect(Object.keys((TransactionActivity as any)._activities)).toHaveLength(0);
58+
jest.runOnlyPendingTimers();
59+
expect(spy).toBeCalledTimes(2);
60+
expect((TransactionActivity as any)._debounce).toBeTruthy();
61+
});
62+
});

0 commit comments

Comments
 (0)