Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions packages/node/src/async/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,75 @@ export function setHooksAsyncContextStrategy(): void {

setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext });
}
/**
* Initiates the counting of created promises and settled promises.
*
* If `locations` is true, it will attempt to find the locations of each created promise
* and return an object whose keys are the callsites as strings and whose values are the
* number of promises created at that callsite.
*
* If `continuation` is true, promises will only be counted if there is an async
* continuation chain (as determined by AsyncLocalStorage) back to the given `startCounter()` call.
* This is helpful for filtering out unrelated promises, like ones from an unrelated concurrent HTTP request.
*
* @param {Object} opts - Options for counting promises.
* @param {boolean} [opts.locations=false] - Whether to count promise locations.
* @param {boolean} [opts.continuation=false] - Whether to consider async continuation chains.
* @returns {function(): number | { [key: string]: number }} - A function to get the count of created and settled promises.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @returns {function(): number | { [key: string]: number }} - A function to get the count of created and settled promises.
* @returns {() => number | Record<string, number>} - A function to get the count of created and settled promises.

*/
/**
*
*/
Comment on lines +69 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
*
*/

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing of these lines gives eslint error on line 72 function

Missing JSDoc comment.eslintjsdoc/require-jsdoc

export function startCounter(
opts: { locations: boolean; continuation: boolean } = { locations: false, continuation: false }
): () => { created: number; settled: number; locations: { [key: string]: number } } {
const resultObject: { created: number; settled: number; locations: { [key: string]: number } } = {
created: 0,
settled: 0,
locations: {},
};

function getLocation(): string {
const stack = new Error().stack?.split('\n');
let stackIndex = 2;
let line = stack?.[stackIndex];
while (
line &&
(line.includes(__filename) ||
line.includes('node:internal/promise_hooks') ||
line?.endsWith('<anonymous>)'))
) {
line = stack?.[++stackIndex];
}
return (line || stack?.pop())?.substring(7) || '';
}

const init: () => void = () => {
if (opts.locations) {
const line = getLocation();
resultObject.locations[line] = 1 + (resultObject.locations[line] || 0);
}
}

if (opts.locations) {
init();
}

return () => {
return opts.locations
? resultObject
: {
created: resultObject.created || 0,
settled: resultObject.settled || 0,
locations: resultObject.locations,
};
};
}

// Example usage:
// 1. Call setHooksAsyncContextStrategy() in your application's entry point.
// 2. Use startCounter() to get a function that tracks promises.
// const trackPromises = startCounter({ locations: true, continuation: true });
// const promise1 = new Promise(resolve => setTimeout(resolve, 100));
// const promise2 = new Promise(resolve => setTimeout(resolve, 200));
// trackPromises(); // Call this to get the promise tracking results.
62 changes: 61 additions & 1 deletion packages/node/test/async/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just disable in specific lines, rather than in all file?

import type { Hub } from '@sentry/core';
import { getCurrentHub, runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core';

import { setHooksAsyncContextStrategy } from '../../src/async/hooks';
import { setHooksAsyncContextStrategy , startCounter } from '../../src/async/hooks';
import { conditionalTest } from '../utils';

conditionalTest({ min: 12 })('async_hooks', () => {
Expand Down Expand Up @@ -154,3 +155,62 @@ conditionalTest({ min: 12 })('async_hooks', () => {
});
});
});

describe('startCounter', () => {
test('should track created and settled promises when locations and continuation are true', async () => {
const trackPromises = startCounter({ locations: true, continuation: true });

// Simulate promise creation and settlement
const promise1 = new Promise((resolve) => setTimeout(resolve, 100));
const promise2 = new Promise((resolve) => setTimeout(resolve, 200));

// Manually advance timers to ensure promises settle
jest.advanceTimersByTime(300);

// Call trackPromises to get the results
const result = trackPromises();

// Assert expected results
expect(result.created).toBe(2); // Two promises were created
expect(result.settled).toBe(2); // Two promises were settled
expect(result.locations).toBeDefined(); // Locations are tracked
});

test('should track created and settled promises when locations is true and continuation is false', async () => {
const trackPromises = startCounter({ locations: true, continuation: false });

// Simulate promise creation and settlement
const promise1 = new Promise((resolve) => setTimeout(resolve, 100));
const promise2 = new Promise((resolve) => setTimeout(resolve, 200));

// Wait for promises to settle
await Promise.all([promise1, promise2]);

// Call trackPromises to get the results
const result = trackPromises();

// Assert expected results
expect(result.created).toBe(2); // Two promises were created
expect(result.settled).toBe(2); // Two promises were settled
expect(result.locations).toBeDefined(); // Locations are tracked
});

test('should track promises when locations and continuation are false', async () => {
const trackPromises = startCounter({ locations: false, continuation: false });

// Simulate promise creation and settlement
const promise1 = new Promise((resolve) => setTimeout(resolve, 100));
const promise2 = new Promise((resolve) => setTimeout(resolve, 200));

// Wait for promises to settle
await Promise.all([promise1, promise2]);

// Call trackPromises to get the results
const result = trackPromises();

// Assert expected results
expect(result.created).toBe(2); // Two promises were created
expect(result.settled).toBe(2); // Two promises were settled
expect(result.locations).toEqual({}); // Locations are not tracked
});
});
Loading