From c022264f809510346cd9e4aa0d7d2bfab8098a17 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 17:20:56 +0300 Subject: [PATCH 01/37] Implement perf events --- README.md | 151 ++++++++++++++++++++++++++++++- src/catcher.ts | 43 +++++++++ src/modules/performance.ts | 131 +++++++++++++++++++++++++++ src/modules/socket.ts | 10 +- src/types/performance-message.ts | 32 +++++++ src/types/span.ts | 39 ++++++++ src/types/transaction.ts | 44 +++++++++ 7 files changed, 446 insertions(+), 4 deletions(-) create mode 100644 src/modules/performance.ts create mode 100644 src/types/performance-message.ts create mode 100644 src/types/span.ts create mode 100644 src/types/transaction.ts diff --git a/README.md b/README.md index 454b498..a0458b3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Then import `@hawk.so/javascript` module to your code. ```js import HawkCatcher from '@hawk.so/javascript'; -```` +``` ### Load from CDN @@ -151,7 +151,6 @@ const hawk = new HawkCatcher({ or pass it any moment after Hawk Catcher was instantiated: - ```js import Vue from 'vue'; @@ -161,3 +160,151 @@ const hawk = new HawkCatcher({ hawk.connectVue(Vue) ``` + +## Performance Monitoring + +Hawk JavaScript Catcher also provides performance monitoring capabilities. You can track transactions and spans to measure the performance of your application. + +### Basic Usage + +```javascript +const hawk = new Catcher('your-integration-token'); + +// Start a transaction +const transaction = hawk.startTransaction('page-load', { + route: '/dashboard', + method: 'GET' +}); + +// Create a span for measuring API call +const apiSpan = hawk.startSpan(transaction.id, 'api-request', { + url: '/api/users', + method: 'GET' +}); + +// Do some API work... + +// Finish the span when API call is done +hawk.finishSpan(apiSpan.id); + +// Create another span for data processing +const processSpan = hawk.startSpan(transaction.id, 'process-data'); + +// Do processing work... + +// Finish the processing span +hawk.finishSpan(processSpan.id); + +// Finish the transaction +hawk.finishTransaction(transaction.id); +``` + +### API Reference + +#### startTransaction(name: string, tags?: Record): Transaction + +Starts a new transaction. A transaction represents a high-level operation like a page load or an API request. + +- `name`: Name of the transaction +- `tags`: Optional key-value pairs for additional transaction data + +#### startSpan(transactionId: string, name: string, metadata?: Record): Span + +Creates a new span within a transaction. Spans represent smaller units of work within a transaction. + +- `transactionId`: ID of the parent transaction +- `name`: Name of the span +- `metadata`: Optional metadata for the span + +#### finishSpan(spanId: string): void + +Finishes a span and calculates its duration. + +- `spanId`: ID of the span to finish + +#### finishTransaction(transactionId: string): void + +Finishes a transaction, calculates its duration, and sends the performance data to Hawk. + +- `transactionId`: ID of the transaction to finish + +### Data Model + +#### Transaction +```typescript +interface Transaction { + id: string; + traceId: string; + name: string; + startTime: number; + endTime?: number; + duration?: number; + tags: Record; + spans: Span[]; +} +``` + +#### Span +```typescript +interface Span { + id: string; + transactionId: string; + name: string; + startTime: number; + endTime?: number; + duration?: number; + metadata?: Record; +} +``` + +### Examples + +#### Measuring Route Changes in Vue.js +```javascript +import { Catcher } from '@hawk.so/javascript'; +import Vue from 'vue'; +import Router from 'vue-router'; + +const hawk = new Catcher('your-integration-token'); + +router.beforeEach((to, from, next) => { + const transaction = hawk.startTransaction('route-change', { + from: from.path, + to: to.path + }); + + next(); + + // After route change is complete + Vue.nextTick(() => { + hawk.finishTransaction(transaction.id); + }); +}); +``` + +#### Measuring API Calls +```javascript +async function fetchUsers() { + const transaction = hawk.startTransaction('fetch-users'); + + const apiSpan = hawk.startSpan(transaction.id, 'api-call', { + url: '/api/users', + method: 'GET' + }); + + try { + const response = await fetch('/api/users'); + const data = await response.json(); + + hawk.finishSpan(apiSpan.id); + + const processSpan = hawk.startSpan(transaction.id, 'process-data'); + // Process data... + hawk.finishSpan(processSpan.id); + + return data; + } finally { + hawk.finishTransaction(transaction.id); + } +} +``` diff --git a/src/catcher.ts b/src/catcher.ts index 5a77243..3581f50 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -16,12 +16,16 @@ import type { JavaScriptCatcherIntegrations } from './types/integrations'; import { EventRejectedError } from './errors'; import type { HawkJavaScriptEvent } from './types'; import { isErrorProcessed, markErrorAsProcessed } from './utils/event'; +import PerformanceMonitoring from './modules/performance'; +import type { Transaction } from './types/transaction'; +import type { Span } from './types/span'; /** * Allow to use global VERSION, that will be overwritten by Webpack */ declare const VERSION: string; + /** * Hawk JavaScript Catcher * Module for errors and exceptions tracking @@ -91,6 +95,12 @@ export default class Catcher { */ private readonly disableVueErrorHandler: boolean = false; + + /** + * Performance monitoring instance + */ + private readonly performance: PerformanceMonitoring; + /** * Catcher constructor * @@ -145,6 +155,11 @@ export default class Catcher { if (settings.vue) { this.connectVue(settings.vue); } + + /** + * Init performance monitoring + */ + this.performance = new PerformanceMonitoring(this.transport, this.token, this.version); } /** @@ -542,4 +557,32 @@ export default class Catcher { private appendIntegrationAddons(errorFormatted: CatcherMessage, integrationAddons: JavaScriptCatcherIntegrations): void { Object.assign(errorFormatted.payload.addons, integrationAddons); } + + /** + * Starts a new transaction + */ + public startTransaction(name: string, tags: Record = {}): Transaction { + return this.performance.startTransaction(name, tags); + } + + /** + * Starts a new span within a transaction + */ + public startSpan(transactionId: string, name: string, metadata?: Record): Span { + return this.performance.startSpan(transactionId, name, metadata); + } + + /** + * Finishes a span + */ + public finishSpan(spanId: string): void { + this.performance.finishSpan(spanId); + } + + /** + * Finishes a transaction + */ + public finishTransaction(transactionId: string): void { + this.performance.finishTransaction(transactionId); + } } diff --git a/src/modules/performance.ts b/src/modules/performance.ts new file mode 100644 index 0000000..1f5f27a --- /dev/null +++ b/src/modules/performance.ts @@ -0,0 +1,131 @@ +import { PerformanceMessage } from '../types/performance-message'; +import type { Transaction } from '../types/transaction'; +import type { Span } from '../types/span'; +import { id } from '../utils/id'; +import log from '../utils/log'; +import type Socket from './socket'; + +/** + * Class for managing performance monitoring + */ +export default class PerformanceMonitoring { + /** + * Active transactions map + */ + private activeTransactions: Map = new Map(); + + /** + * Active spans map + */ + private activeSpans: Map = new Map(); + + /** + * @param transport - Transport instance for sending data + * @param token - Integration token + * @param version - Catcher version + */ + constructor( + private readonly transport: Socket, + private readonly token: string, + private readonly version: string + ) {} + + /** + * Starts a new transaction + * + * @param name - Transaction name + * @param tags - Optional tags for the transaction + * @returns Transaction object + */ + public startTransaction(name: string, tags: Record = {}): Transaction { + const transaction: Transaction = { + id: id(), + traceId: id(), + name, + startTime: performance.now(), + tags, + spans: [] + }; + + this.activeTransactions.set(transaction.id, transaction); + return transaction; + } + + /** + * Starts a new span within a transaction + * + * @param transactionId - Parent transaction ID + * @param name - Span name + * @param metadata - Optional metadata for the span + * @returns Span object + */ + public startSpan(transactionId: string, name: string, metadata?: Record): Span { + const span: Span = { + id: id(), + transactionId, + name, + startTime: performance.now(), + metadata + }; + + this.activeSpans.set(span.id, span); + const transaction = this.activeTransactions.get(transactionId); + + if (transaction) { + transaction.spans.push(span); + } + + return span; + } + + /** + * Finishes a span and calculates its duration + * + * @param spanId - ID of the span to finish + */ + public finishSpan(spanId: string): void { + const span = this.activeSpans.get(spanId); + if (span) { + span.endTime = performance.now(); + span.duration = span.endTime - span.startTime; + this.activeSpans.delete(spanId); + } + } + + /** + * Finishes a transaction, calculates its duration and sends the data + * + * @param transactionId - ID of the transaction to finish + */ + public finishTransaction(transactionId: string): void { + const transaction = this.activeTransactions.get(transactionId); + if (transaction) { + transaction.endTime = performance.now(); + transaction.duration = transaction.endTime - transaction.startTime; + this.activeTransactions.delete(transactionId); + + this.sendPerformanceData(transaction); + } + } + + /** + * Sends performance data to Hawk collector + * + * @param transaction - Transaction data to send + */ + private sendPerformanceData(transaction: Transaction): void { + const performanceMessage: PerformanceMessage = { + token: this.token, + catcherType: 'performance', + payload: { + ...transaction, + catcherVersion: this.version + } + }; + + this.transport.send(performanceMessage) + .catch((error) => { + log('Failed to send performance data', 'error', error); + }); + } +} diff --git a/src/modules/socket.ts b/src/modules/socket.ts index c07af1d..5303aae 100644 --- a/src/modules/socket.ts +++ b/src/modules/socket.ts @@ -1,6 +1,12 @@ +import { PerformanceMessage } from 'src/types/performance-message'; import log from '../utils/log'; import type { CatcherMessage } from '@/types'; +/** + * Supported message types + */ +export type MessageType = 'errors/javascript' | 'performance'; + /** * Custom WebSocket wrapper class * @@ -31,7 +37,7 @@ export default class Socket { * Queue of events collected while socket is not connected * They will be sent when connection will be established */ - private eventsQueue: CatcherMessage[]; + private eventsQueue: (CatcherMessage | PerformanceMessage)[]; /** * Websocket instance @@ -96,7 +102,7 @@ export default class Socket { * * @param message - event data in Hawk Format */ - public async send(message: CatcherMessage): Promise { + public async send(message: CatcherMessage | PerformanceMessage): Promise { if (this.ws === null) { this.eventsQueue.push(message); diff --git a/src/types/performance-message.ts b/src/types/performance-message.ts new file mode 100644 index 0000000..1872531 --- /dev/null +++ b/src/types/performance-message.ts @@ -0,0 +1,32 @@ +import type { Transaction } from './transaction'; +import type { EncodedIntegrationToken } from '@hawk.so/types'; + +/** + * Interface for performance monitoring message payload + */ +export interface PerformancePayload extends Transaction { + /** + * Version of the catcher that sent this message + */ + catcherVersion: string; +} + +/** + * Interface for performance monitoring message + */ +export interface PerformanceMessage { + /** + * Integration token + */ + token: EncodedIntegrationToken; + + /** + * Type of the catcher that sent this message + */ + catcherType: 'performance'; + + /** + * Performance monitoring data + */ + payload: PerformancePayload; +} diff --git a/src/types/span.ts b/src/types/span.ts new file mode 100644 index 0000000..7208419 --- /dev/null +++ b/src/types/span.ts @@ -0,0 +1,39 @@ +/** + * Interface for Span data + */ +export interface Span { + /** + * Unique identifier of the span + */ + id: string; + + /** + * ID of the parent transaction + */ + transactionId: string; + + /** + * Name of the span (e.g., 'db-query', 'http-request') + */ + name: string; + + /** + * Timestamp when the span started + */ + startTime: number; + + /** + * Timestamp when the span ended + */ + endTime?: number; + + /** + * Total duration of the span in milliseconds + */ + duration?: number; + + /** + * Additional context data for the span + */ + metadata?: Record; +} diff --git a/src/types/transaction.ts b/src/types/transaction.ts new file mode 100644 index 0000000..e163d33 --- /dev/null +++ b/src/types/transaction.ts @@ -0,0 +1,44 @@ +/** + * Interface for Transaction data + */ +export interface Transaction { + /** + * Unique identifier of the transaction + */ + id: string; + + /** + * Identifier for grouping related transactions + */ + traceId: string; + + /** + * Name of the transaction (e.g., 'page-load', 'api-request') + */ + name: string; + + /** + * Timestamp when the transaction started + */ + startTime: number; + + /** + * Timestamp when the transaction ended + */ + endTime?: number; + + /** + * Total duration of the transaction in milliseconds + */ + duration?: number; + + /** + * Key-value pairs for additional transaction data + */ + tags: Record; + + /** + * List of spans associated with this transaction + */ + spans: Span[]; +} From 0bc009346e94adf9242103d3c1e1325bc14baa52 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 17:46:07 +0300 Subject: [PATCH 02/37] feat(performance): enhance performance monitoring with batch sending and resource cleanup --- src/catcher.ts | 14 ++- src/modules/performance.ts | 188 ++++++++++++++++++++++++++++++++++--- src/types/transaction.ts | 2 + 3 files changed, 190 insertions(+), 14 deletions(-) diff --git a/src/catcher.ts b/src/catcher.ts index 3581f50..5451734 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -159,7 +159,12 @@ export default class Catcher { /** * Init performance monitoring */ - this.performance = new PerformanceMonitoring(this.transport, this.token, this.version); + this.performance = new PerformanceMonitoring( + this.transport, + this.token, + this.version, + this.debug + ); } /** @@ -585,4 +590,11 @@ export default class Catcher { public finishTransaction(transactionId: string): void { this.performance.finishTransaction(transactionId); } + + /** + * Clean up resources + */ + public destroy(): void { + this.performance.destroy(); + } } diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 1f5f27a..e430676 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -5,6 +5,31 @@ import { id } from '../utils/id'; import log from '../utils/log'; import type Socket from './socket'; +/** + * Default batch sending interval in milliseconds + */ +const BATCH_INTERVAL = 5000; + +/** + * Check if code is running in browser environment + */ +const isBrowser = typeof window !== 'undefined'; + +/** + * Get high-resolution timestamp in milliseconds + */ +const getTimestamp = (): number => { + if (isBrowser) { + return performance.now(); + } + + /** + * process.hrtime.bigint() returns nanoseconds + * Convert to milliseconds for consistency with browser + */ + return Number(process.hrtime.bigint() / BigInt(1_000_000)); +}; + /** * Class for managing performance monitoring */ @@ -19,16 +44,130 @@ export default class PerformanceMonitoring { */ private activeSpans: Map = new Map(); + /** + * Queue of completed transactions waiting to be sent + */ + private queue: Transaction[] = []; + + /** + * Timer for batch sending + */ + private batchTimeout: number | NodeJS.Timeout | null = null; + + /** + * Flag indicating if we're in the process of sending data + */ + private isSending = false; + /** * @param transport - Transport instance for sending data * @param token - Integration token * @param version - Catcher version + * @param debug - Debug mode flag */ constructor( private readonly transport: Socket, private readonly token: string, - private readonly version: string - ) {} + private readonly version: string, + private readonly debug: boolean = false + ) { + if (isBrowser) { + this.initBeforeUnloadHandler(); + } else { + this.initProcessExitHandler(); + } + + this.startBatchSending(); + } + + /** + * Initialize handler for browser page unload + */ + private initBeforeUnloadHandler(): void { + window.addEventListener('beforeunload', () => { + this.flushQueue(); + }); + } + + /** + * Initialize handler for Node.js process exit + */ + private initProcessExitHandler(): void { + process.on('beforeExit', async () => { + await this.flushQueue(); + }); + + // Handle SIGINT and SIGTERM + ['SIGINT', 'SIGTERM'].forEach(signal => { + process.on(signal, async () => { + await this.flushQueue(); + process.exit(0); + }); + }); + } + + /** + * Start batch sending timer + */ + private startBatchSending(): void { + if (this.batchTimeout !== null) { + return; + } + + const timer = isBrowser ? window.setInterval : setInterval; + this.batchTimeout = timer(() => { + void this.sendBatch(); + }, BATCH_INTERVAL); + } + + /** + * Stop batch sending timer + */ + private stopBatchSending(): void { + if (this.batchTimeout !== null) { + if (isBrowser) { + window.clearInterval(this.batchTimeout as number); + } else { + clearInterval(this.batchTimeout as NodeJS.Timeout); + } + this.batchTimeout = null; + } + } + + /** + * Send batch of transactions + */ + private async sendBatch(): Promise { + if (this.isSending || this.queue.length === 0) { + return; + } + + this.isSending = true; + + const batch = this.queue; + + try { + this.queue = []; + + await Promise.all( + batch.map(transaction => this.sendPerformanceData(transaction)) + ); + } catch (error) { + log('Failed to send performance data batch', 'error', error); + // Return failed transactions to the queue + this.queue.unshift(...batch); + } finally { + this.isSending = false; + } + } + + /** + * Immediately send all queued transactions + */ + private async flushQueue(): Promise { + this.stopBatchSending(); + await this.sendBatch(); + } /** * Starts a new transaction @@ -42,7 +181,7 @@ export default class PerformanceMonitoring { id: id(), traceId: id(), name, - startTime: performance.now(), + startTime: getTimestamp(), tags, spans: [] }; @@ -64,7 +203,7 @@ export default class PerformanceMonitoring { id: id(), transactionId, name, - startTime: performance.now(), + startTime: getTimestamp(), metadata }; @@ -86,25 +225,44 @@ export default class PerformanceMonitoring { public finishSpan(spanId: string): void { const span = this.activeSpans.get(spanId); if (span) { - span.endTime = performance.now(); + span.endTime = getTimestamp(); span.duration = span.endTime - span.startTime; this.activeSpans.delete(spanId); } } /** - * Finishes a transaction, calculates its duration and sends the data + * Finishes a transaction, calculates its duration and queues it for sending * * @param transactionId - ID of the transaction to finish */ public finishTransaction(transactionId: string): void { const transaction = this.activeTransactions.get(transactionId); if (transaction) { - transaction.endTime = performance.now(); + // Finish all active spans belonging to this transaction + transaction.spans.forEach(span => { + if (!span.endTime) { + if (this.debug) { + log(`Automatically finishing uncompleted span "${span.name}" in transaction "${transaction.name}"`, 'warn'); + } + + const activeSpan = this.activeSpans.get(span.id); + if (activeSpan) { + activeSpan.endTime = getTimestamp(); + activeSpan.duration = activeSpan.endTime - activeSpan.startTime; + this.activeSpans.delete(span.id); + + // Update span in transaction with final data + Object.assign(span, activeSpan); + } + } + }); + + transaction.endTime = getTimestamp(); transaction.duration = transaction.endTime - transaction.startTime; this.activeTransactions.delete(transactionId); - this.sendPerformanceData(transaction); + this.queue.push(transaction); } } @@ -113,7 +271,7 @@ export default class PerformanceMonitoring { * * @param transaction - Transaction data to send */ - private sendPerformanceData(transaction: Transaction): void { + private async sendPerformanceData(transaction: Transaction): Promise { const performanceMessage: PerformanceMessage = { token: this.token, catcherType: 'performance', @@ -123,9 +281,13 @@ export default class PerformanceMonitoring { } }; - this.transport.send(performanceMessage) - .catch((error) => { - log('Failed to send performance data', 'error', error); - }); + await this.transport.send(performanceMessage); + } + + /** + * Clean up resources + */ + public destroy(): void { + this.stopBatchSending(); } } diff --git a/src/types/transaction.ts b/src/types/transaction.ts index e163d33..a80521e 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -1,3 +1,5 @@ +import { Span } from "./span"; + /** * Interface for Transaction data */ From 767592b8449b0aabf0290156a4104e4d2dec645a Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 17:54:17 +0300 Subject: [PATCH 03/37] Lint --- src/catcher.ts | 15 +++++++++++++-- src/modules/performance.ts | 31 ++++++++++++++++++------------- src/modules/stackParser.ts | 2 +- src/types/span.ts | 2 +- src/types/transaction.ts | 4 ++-- tsconfig.json | 4 ++-- 6 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/catcher.ts b/src/catcher.ts index 5451734..1a2efda 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -160,8 +160,8 @@ export default class Catcher { * Init performance monitoring */ this.performance = new PerformanceMonitoring( - this.transport, - this.token, + this.transport, + this.token, this.version, this.debug ); @@ -565,6 +565,9 @@ export default class Catcher { /** * Starts a new transaction + * + * @param name + * @param tags */ public startTransaction(name: string, tags: Record = {}): Transaction { return this.performance.startTransaction(name, tags); @@ -572,6 +575,10 @@ export default class Catcher { /** * Starts a new span within a transaction + * + * @param transactionId + * @param name + * @param metadata */ public startSpan(transactionId: string, name: string, metadata?: Record): Span { return this.performance.startSpan(transactionId, name, metadata); @@ -579,6 +586,8 @@ export default class Catcher { /** * Finishes a span + * + * @param spanId */ public finishSpan(spanId: string): void { this.performance.finishSpan(spanId); @@ -586,6 +595,8 @@ export default class Catcher { /** * Finishes a transaction + * + * @param transactionId */ public finishTransaction(transactionId: string): void { this.performance.finishTransaction(transactionId); diff --git a/src/modules/performance.ts b/src/modules/performance.ts index e430676..ef87bce 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -76,7 +76,7 @@ export default class PerformanceMonitoring { } else { this.initProcessExitHandler(); } - + this.startBatchSending(); } @@ -115,6 +115,7 @@ export default class PerformanceMonitoring { } const timer = isBrowser ? window.setInterval : setInterval; + this.batchTimeout = timer(() => { void this.sendBatch(); }, BATCH_INTERVAL); @@ -145,7 +146,7 @@ export default class PerformanceMonitoring { this.isSending = true; const batch = this.queue; - + try { this.queue = []; @@ -171,7 +172,7 @@ export default class PerformanceMonitoring { /** * Starts a new transaction - * + * * @param name - Transaction name * @param tags - Optional tags for the transaction * @returns Transaction object @@ -183,16 +184,17 @@ export default class PerformanceMonitoring { name, startTime: getTimestamp(), tags, - spans: [] + spans: [], }; this.activeTransactions.set(transaction.id, transaction); + return transaction; } /** * Starts a new span within a transaction - * + * * @param transactionId - Parent transaction ID * @param name - Span name * @param metadata - Optional metadata for the span @@ -204,7 +206,7 @@ export default class PerformanceMonitoring { transactionId, name, startTime: getTimestamp(), - metadata + metadata, }; this.activeSpans.set(span.id, span); @@ -219,11 +221,12 @@ export default class PerformanceMonitoring { /** * Finishes a span and calculates its duration - * + * * @param spanId - ID of the span to finish */ public finishSpan(spanId: string): void { const span = this.activeSpans.get(spanId); + if (span) { span.endTime = getTimestamp(); span.duration = span.endTime - span.startTime; @@ -233,11 +236,12 @@ export default class PerformanceMonitoring { /** * Finishes a transaction, calculates its duration and queues it for sending - * + * * @param transactionId - ID of the transaction to finish */ public finishTransaction(transactionId: string): void { const transaction = this.activeTransactions.get(transactionId); + if (transaction) { // Finish all active spans belonging to this transaction transaction.spans.forEach(span => { @@ -247,6 +251,7 @@ export default class PerformanceMonitoring { } const activeSpan = this.activeSpans.get(span.id); + if (activeSpan) { activeSpan.endTime = getTimestamp(); activeSpan.duration = activeSpan.endTime - activeSpan.startTime; @@ -261,14 +266,14 @@ export default class PerformanceMonitoring { transaction.endTime = getTimestamp(); transaction.duration = transaction.endTime - transaction.startTime; this.activeTransactions.delete(transactionId); - + this.queue.push(transaction); } } /** * Sends performance data to Hawk collector - * + * * @param transaction - Transaction data to send */ private async sendPerformanceData(transaction: Transaction): Promise { @@ -277,8 +282,8 @@ export default class PerformanceMonitoring { catcherType: 'performance', payload: { ...transaction, - catcherVersion: this.version - } + catcherVersion: this.version, + }, }; await this.transport.send(performanceMessage); @@ -290,4 +295,4 @@ export default class PerformanceMonitoring { public destroy(): void { this.stopBatchSending(); } -} +} diff --git a/src/modules/stackParser.ts b/src/modules/stackParser.ts index 4352288..729ef8c 100644 --- a/src/modules/stackParser.ts +++ b/src/modules/stackParser.ts @@ -47,7 +47,7 @@ export default class StackParser { try { if (!frame.fileName) { return null; - }; + } if (!this.isValidUrl(frame.fileName)) { return null; diff --git a/src/types/span.ts b/src/types/span.ts index 7208419..338425c 100644 --- a/src/types/span.ts +++ b/src/types/span.ts @@ -36,4 +36,4 @@ export interface Span { * Additional context data for the span */ metadata?: Record; -} +} diff --git a/src/types/transaction.ts b/src/types/transaction.ts index a80521e..b3c622a 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -1,4 +1,4 @@ -import { Span } from "./span"; +import { Span } from './span'; /** * Interface for Transaction data @@ -43,4 +43,4 @@ export interface Transaction { * List of spans associated with this transaction */ spans: Span[]; -} +} diff --git a/tsconfig.json b/tsconfig.json index 9624c27..6387ab3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions" : { + "module": "es2015", "strict": false, "strictNullChecks": true, "sourceMap": true, @@ -7,8 +8,7 @@ "declaration": true, "outDir": "dist", "rootDir": "src", - "module": "NodeNext", - "moduleResolution": "nodenext", + "moduleResolution": "classic", "lib": ["dom", "es2017", "es2018"], "baseUrl": ".", "paths": { From ab1ab010cbbc858be183780521c3bd760dff53a0 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 19:00:14 +0300 Subject: [PATCH 04/37] Upd --- example/index.html | 94 +++++++++++++++++++++++++++++++- src/catcher.ts | 14 ++--- src/modules/performance.ts | 106 ++++--------------------------------- 3 files changed, 110 insertions(+), 104 deletions(-) diff --git a/example/index.html b/example/index.html index 6f5b6cb..f2f34a6 100644 --- a/example/index.html +++ b/example/index.html @@ -84,6 +84,60 @@ font-size: 15px; color:inherit; } + + .performance-demo { + border-radius: 4px; + } + + .performance-demo button { + padding: 8px 16px; + padding-right: 32px; + background: #4979E4; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + position: relative; + min-width: 200px; + text-align: left; + } + + .performance-demo button:hover { + background: #4869d2; + } + + .performance-demo button[disabled] { + background: #4979E4; + opacity: 0.7; + cursor: not-allowed; + } + + .performance-demo button .spinner { + display: none; + width: 10px; + height: 10px; + border: 2px solid #fff; + border-bottom-color: transparent; + border-radius: 50%; + animation: rotation 1s linear infinite; + position: absolute; + right: 10px; + top: 50%; + margin-top: -5px; + } + + .performance-demo button[disabled] .spinner { + display: block; + } + + @keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } @@ -152,7 +206,17 @@

Test console catcher



- +
+

Performance Monitoring Demo

+
+ +
+
+ +

Test Vue integration: $root

@@ -196,7 +260,7 @@

Test Vue integration: <test-component>

el: tag, instance: new Editor(), classProto: Editor, - longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', + longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', } }, props: { @@ -236,5 +300,31 @@

Test Vue integration: <test-component>

}, }) + diff --git a/src/catcher.ts b/src/catcher.ts index 1a2efda..3796e8b 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -566,8 +566,8 @@ export default class Catcher { /** * Starts a new transaction * - * @param name - * @param tags + * @param name - Name of the transaction (e.g., 'page-load', 'api-request') + * @param tags - Key-value pairs for additional transaction data */ public startTransaction(name: string, tags: Record = {}): Transaction { return this.performance.startTransaction(name, tags); @@ -576,9 +576,9 @@ export default class Catcher { /** * Starts a new span within a transaction * - * @param transactionId - * @param name - * @param metadata + * @param transactionId - ID of the parent transaction this span belongs to + * @param name - Name of the span (e.g., 'db-query', 'http-request') + * @param metadata - Additional context data for the span */ public startSpan(transactionId: string, name: string, metadata?: Record): Span { return this.performance.startSpan(transactionId, name, metadata); @@ -587,7 +587,7 @@ export default class Catcher { /** * Finishes a span * - * @param spanId + * @param spanId - ID of the span to finish */ public finishSpan(spanId: string): void { this.performance.finishSpan(spanId); @@ -596,7 +596,7 @@ export default class Catcher { /** * Finishes a transaction * - * @param transactionId + * @param transactionId - ID of the transaction to finish */ public finishTransaction(transactionId: string): void { this.performance.finishTransaction(transactionId); diff --git a/src/modules/performance.ts b/src/modules/performance.ts index ef87bce..528121a 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -5,11 +5,6 @@ import { id } from '../utils/id'; import log from '../utils/log'; import type Socket from './socket'; -/** - * Default batch sending interval in milliseconds - */ -const BATCH_INTERVAL = 5000; - /** * Check if code is running in browser environment */ @@ -44,21 +39,6 @@ export default class PerformanceMonitoring { */ private activeSpans: Map = new Map(); - /** - * Queue of completed transactions waiting to be sent - */ - private queue: Transaction[] = []; - - /** - * Timer for batch sending - */ - private batchTimeout: number | NodeJS.Timeout | null = null; - - /** - * Flag indicating if we're in the process of sending data - */ - private isSending = false; - /** * @param transport - Transport instance for sending data * @param token - Integration token @@ -76,8 +56,6 @@ export default class PerformanceMonitoring { } else { this.initProcessExitHandler(); } - - this.startBatchSending(); } /** @@ -85,7 +63,8 @@ export default class PerformanceMonitoring { */ private initBeforeUnloadHandler(): void { window.addEventListener('beforeunload', () => { - this.flushQueue(); + // Finish any active transactions before unload + this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); }); } @@ -93,83 +72,19 @@ export default class PerformanceMonitoring { * Initialize handler for Node.js process exit */ private initProcessExitHandler(): void { - process.on('beforeExit', async () => { - await this.flushQueue(); + process.on('beforeExit', () => { + // Finish any active transactions before exit + this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); }); - // Handle SIGINT and SIGTERM ['SIGINT', 'SIGTERM'].forEach(signal => { - process.on(signal, async () => { - await this.flushQueue(); + process.on(signal, () => { + this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); process.exit(0); }); }); } - /** - * Start batch sending timer - */ - private startBatchSending(): void { - if (this.batchTimeout !== null) { - return; - } - - const timer = isBrowser ? window.setInterval : setInterval; - - this.batchTimeout = timer(() => { - void this.sendBatch(); - }, BATCH_INTERVAL); - } - - /** - * Stop batch sending timer - */ - private stopBatchSending(): void { - if (this.batchTimeout !== null) { - if (isBrowser) { - window.clearInterval(this.batchTimeout as number); - } else { - clearInterval(this.batchTimeout as NodeJS.Timeout); - } - this.batchTimeout = null; - } - } - - /** - * Send batch of transactions - */ - private async sendBatch(): Promise { - if (this.isSending || this.queue.length === 0) { - return; - } - - this.isSending = true; - - const batch = this.queue; - - try { - this.queue = []; - - await Promise.all( - batch.map(transaction => this.sendPerformanceData(transaction)) - ); - } catch (error) { - log('Failed to send performance data batch', 'error', error); - // Return failed transactions to the queue - this.queue.unshift(...batch); - } finally { - this.isSending = false; - } - } - - /** - * Immediately send all queued transactions - */ - private async flushQueue(): Promise { - this.stopBatchSending(); - await this.sendBatch(); - } - /** * Starts a new transaction * @@ -235,7 +150,7 @@ export default class PerformanceMonitoring { } /** - * Finishes a transaction, calculates its duration and queues it for sending + * Finishes a transaction, calculates its duration and sends it * * @param transactionId - ID of the transaction to finish */ @@ -267,7 +182,7 @@ export default class PerformanceMonitoring { transaction.duration = transaction.endTime - transaction.startTime; this.activeTransactions.delete(transactionId); - this.queue.push(transaction); + void this.sendPerformanceData(transaction); } } @@ -293,6 +208,7 @@ export default class PerformanceMonitoring { * Clean up resources */ public destroy(): void { - this.stopBatchSending(); + // Finish any remaining transactions + this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); } } From 751a0483b1049c2f5ee222856ae8fb1a0dc8996c Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 19:18:27 +0300 Subject: [PATCH 05/37] Upd --- src/modules/performance.ts | 97 ++++++++++++++++++++++++++++++++++++-- src/modules/socket.ts | 2 +- src/types/transaction.ts | 2 +- tsconfig.json | 4 +- 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 528121a..0a463d5 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -1,4 +1,4 @@ -import { PerformanceMessage } from '../types/performance-message'; +import type { PerformanceMessage } from '../types/performance-message'; import type { Transaction } from '../types/transaction'; import type { Span } from '../types/span'; import { id } from '../utils/id'; @@ -25,6 +25,11 @@ const getTimestamp = (): number => { return Number(process.hrtime.bigint() / BigInt(1_000_000)); }; +/** + * Minimum interval between performance data sends (in milliseconds) + */ +const THROTTLE_INTERVAL = 1000; + /** * Class for managing performance monitoring */ @@ -39,6 +44,21 @@ export default class PerformanceMonitoring { */ private activeSpans: Map = new Map(); + /** + * Queue for transactions waiting to be sent + */ + private sendQueue: Transaction[] = []; + + /** + * Timestamp of last send operation + */ + private lastSendTime = 0; + + /** + * Scheduled send timeout ID + */ + private sendTimeout: number | NodeJS.Timeout | null = null; + /** * @param transport - Transport instance for sending data * @param token - Integration token @@ -85,6 +105,62 @@ export default class PerformanceMonitoring { }); } + /** + * Schedule sending of performance data with throttling + */ + private scheduleSend(): void { + if (this.sendTimeout !== null) { + return; + } + + const now = Date.now(); + const timeSinceLastSend = now - this.lastSendTime; + const delay = Math.max(0, THROTTLE_INTERVAL - timeSinceLastSend); + + const timer = isBrowser ? window.setTimeout : setTimeout; + + this.sendTimeout = timer(() => { + void this.processSendQueue(); + }, delay); + } + + /** + * Process queued transactions + */ + private async processSendQueue(): Promise { + if (this.sendQueue.length === 0) { + this.sendTimeout = null; + + return; + } + + try { + const transaction = this.sendQueue.shift()!; + + await this.sendPerformanceData(transaction); + this.lastSendTime = Date.now(); + } catch (error) { + if (this.debug) { + log('Failed to send performance data', 'error', error); + } + } finally { + this.sendTimeout = null; + if (this.sendQueue.length > 0) { + this.scheduleSend(); + } + } + } + + /** + * Queue transaction for sending + * + * @param transaction + */ + private queueTransaction(transaction: Transaction): void { + this.sendQueue.push(transaction); + this.scheduleSend(); + } + /** * Starts a new transaction * @@ -182,7 +258,7 @@ export default class PerformanceMonitoring { transaction.duration = transaction.endTime - transaction.startTime; this.activeTransactions.delete(transactionId); - void this.sendPerformanceData(transaction); + this.queueTransaction(transaction); } } @@ -205,10 +281,25 @@ export default class PerformanceMonitoring { } /** - * Clean up resources + * Clean up resources and ensure all data is sent */ public destroy(): void { // Finish any remaining transactions this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); + + // Clear any pending send timeout + if (this.sendTimeout !== null) { + if (isBrowser) { + window.clearTimeout(this.sendTimeout as number); + } else { + clearTimeout(this.sendTimeout as NodeJS.Timeout); + } + this.sendTimeout = null; + } + + // Force send any remaining queued data + if (this.sendQueue.length > 0) { + void this.processSendQueue(); + } } } diff --git a/src/modules/socket.ts b/src/modules/socket.ts index 5303aae..ceda58a 100644 --- a/src/modules/socket.ts +++ b/src/modules/socket.ts @@ -1,4 +1,4 @@ -import { PerformanceMessage } from 'src/types/performance-message'; +import type { PerformanceMessage } from 'src/types/performance-message'; import log from '../utils/log'; import type { CatcherMessage } from '@/types'; diff --git a/src/types/transaction.ts b/src/types/transaction.ts index b3c622a..a794a24 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -1,4 +1,4 @@ -import { Span } from './span'; +import type { Span } from './span'; /** * Interface for Transaction data diff --git a/tsconfig.json b/tsconfig.json index 6387ab3..f91e093 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions" : { - "module": "es2015", + "module": "NodeNext", "strict": false, "strictNullChecks": true, "sourceMap": true, @@ -8,7 +8,7 @@ "declaration": true, "outDir": "dist", "rootDir": "src", - "moduleResolution": "classic", + "moduleResolution": "nodenext", "lib": ["dom", "es2017", "es2018"], "baseUrl": ".", "paths": { From 5daeb4e19de171517f592f66977766c7f75548de Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 19:52:01 +0300 Subject: [PATCH 06/37] Upd api --- README.md | 60 ++++++------- example/index.html | 14 +-- src/modules/performance.ts | 173 +++++++++++++++++-------------------- src/types/span.ts | 39 --------- src/types/transaction.ts | 46 ---------- 5 files changed, 113 insertions(+), 219 deletions(-) delete mode 100644 src/types/span.ts delete mode 100644 src/types/transaction.ts diff --git a/README.md b/README.md index a0458b3..46061c5 100644 --- a/README.md +++ b/README.md @@ -163,42 +163,35 @@ hawk.connectVue(Vue) ## Performance Monitoring -Hawk JavaScript Catcher also provides performance monitoring capabilities. You can track transactions and spans to measure the performance of your application. - -### Basic Usage - -```javascript -const hawk = new Catcher('your-integration-token'); +Hawk JavaScript Catcher includes a Performance Monitoring API to track application performance metrics: +```typescript // Start a transaction const transaction = hawk.startTransaction('page-load', { - route: '/dashboard', - method: 'GET' + page: '/home', + type: 'navigation' }); -// Create a span for measuring API call -const apiSpan = hawk.startSpan(transaction.id, 'api-request', { - url: '/api/users', - method: 'GET' +// Create spans within transaction +const span = transaction.startSpan('api-call', { + url: '/api/users' }); -// Do some API work... +// Finish span when operation completes +span.finish(); -// Finish the span when API call is done -hawk.finishSpan(apiSpan.id); - -// Create another span for data processing -const processSpan = hawk.startSpan(transaction.id, 'process-data'); - -// Do processing work... - -// Finish the processing span -hawk.finishSpan(processSpan.id); - -// Finish the transaction -hawk.finishTransaction(transaction.id); +// Finish transaction +transaction.finish(); ``` +Features: +- Track transactions and spans with timing data +- Automatic span completion when transaction ends +- Support for both browser and Node.js environments +- Debug mode for development +- Throttled data sending to prevent server overload +- Graceful cleanup on page unload/process exit + ### API Reference #### startTransaction(name: string, tags?: Record): Transaction @@ -241,6 +234,8 @@ interface Transaction { duration?: number; tags: Record; spans: Span[]; + startSpan(name: string, metadata?: Record): Span; + finish(): void; } ``` @@ -254,6 +249,7 @@ interface Span { endTime?: number; duration?: number; metadata?: Record; + finish(): void; } ``` @@ -277,7 +273,7 @@ router.beforeEach((to, from, next) => { // After route change is complete Vue.nextTick(() => { - hawk.finishTransaction(transaction.id); + transaction.finish(); }); }); ``` @@ -287,7 +283,7 @@ router.beforeEach((to, from, next) => { async function fetchUsers() { const transaction = hawk.startTransaction('fetch-users'); - const apiSpan = hawk.startSpan(transaction.id, 'api-call', { + const apiSpan = transaction.startSpan('api-call', { url: '/api/users', method: 'GET' }); @@ -296,15 +292,15 @@ async function fetchUsers() { const response = await fetch('/api/users'); const data = await response.json(); - hawk.finishSpan(apiSpan.id); + apiSpan.finish(); - const processSpan = hawk.startSpan(transaction.id, 'process-data'); + const processSpan = transaction.startSpan('process-data'); // Process data... - hawk.finishSpan(processSpan.id); + processSpan.finish(); return data; } finally { - hawk.finishTransaction(transaction.id); + transaction.finish(); } } ``` diff --git a/example/index.html b/example/index.html index f2f34a6..abbb269 100644 --- a/example/index.html +++ b/example/index.html @@ -260,7 +260,7 @@

Test Vue integration: <test-component>

el: tag, instance: new Editor(), classProto: Editor, - longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', + longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', } }, props: { @@ -310,17 +310,17 @@

Test Vue integration: <test-component>

type: 'background' }); - const step1 = window.hawk.startSpan(transaction.id, 'step-1'); + const step1 = transaction.startSpan('step-1'); setTimeout(() => { - window.hawk.finishSpan(step1.id); + step1.finish(); - const step2 = window.hawk.startSpan(transaction.id, 'step-2'); + const step2 = transaction.startSpan('step-2'); // Simulate unfinished span - const step3 = window.hawk.startSpan(transaction.id, 'step-3'); + const step3 = transaction.startSpan('step-3'); setTimeout(() => { - window.hawk.finishSpan(step3.id); - window.hawk.finishTransaction(transaction.id); + step3.finish(); + transaction.finish(); button.disabled = false; }, 300); }, 400); diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 0a463d5..750a462 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -1,6 +1,4 @@ import type { PerformanceMessage } from '../types/performance-message'; -import type { Transaction } from '../types/transaction'; -import type { Span } from '../types/span'; import { id } from '../utils/id'; import log from '../utils/log'; import type Socket from './socket'; @@ -39,11 +37,6 @@ export default class PerformanceMonitoring { */ private activeTransactions: Map = new Map(); - /** - * Active spans map - */ - private activeSpans: Map = new Map(); - /** * Queue for transactions waiting to be sent */ @@ -84,7 +77,7 @@ export default class PerformanceMonitoring { private initBeforeUnloadHandler(): void { window.addEventListener('beforeunload', () => { // Finish any active transactions before unload - this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); + this.activeTransactions.forEach(transaction => transaction.finish()); }); } @@ -94,12 +87,12 @@ export default class PerformanceMonitoring { private initProcessExitHandler(): void { process.on('beforeExit', () => { // Finish any active transactions before exit - this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); + this.activeTransactions.forEach(transaction => transaction.finish()); }); ['SIGINT', 'SIGTERM'].forEach(signal => { process.on(signal, () => { - this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); + this.activeTransactions.forEach(transaction => transaction.finish()); process.exit(0); }); }); @@ -153,10 +146,9 @@ export default class PerformanceMonitoring { /** * Queue transaction for sending - * - * @param transaction */ - private queueTransaction(transaction: Transaction): void { + public queueTransaction(transaction: Transaction): void { + this.activeTransactions.delete(transaction.id); this.sendQueue.push(transaction); this.scheduleSend(); } @@ -169,7 +161,7 @@ export default class PerformanceMonitoring { * @returns Transaction object */ public startTransaction(name: string, tags: Record = {}): Transaction { - const transaction: Transaction = { + const data = { id: id(), traceId: id(), name, @@ -178,90 +170,11 @@ export default class PerformanceMonitoring { spans: [], }; + const transaction = new Transaction(data, this); this.activeTransactions.set(transaction.id, transaction); - return transaction; } - /** - * Starts a new span within a transaction - * - * @param transactionId - Parent transaction ID - * @param name - Span name - * @param metadata - Optional metadata for the span - * @returns Span object - */ - public startSpan(transactionId: string, name: string, metadata?: Record): Span { - const span: Span = { - id: id(), - transactionId, - name, - startTime: getTimestamp(), - metadata, - }; - - this.activeSpans.set(span.id, span); - const transaction = this.activeTransactions.get(transactionId); - - if (transaction) { - transaction.spans.push(span); - } - - return span; - } - - /** - * Finishes a span and calculates its duration - * - * @param spanId - ID of the span to finish - */ - public finishSpan(spanId: string): void { - const span = this.activeSpans.get(spanId); - - if (span) { - span.endTime = getTimestamp(); - span.duration = span.endTime - span.startTime; - this.activeSpans.delete(spanId); - } - } - - /** - * Finishes a transaction, calculates its duration and sends it - * - * @param transactionId - ID of the transaction to finish - */ - public finishTransaction(transactionId: string): void { - const transaction = this.activeTransactions.get(transactionId); - - if (transaction) { - // Finish all active spans belonging to this transaction - transaction.spans.forEach(span => { - if (!span.endTime) { - if (this.debug) { - log(`Automatically finishing uncompleted span "${span.name}" in transaction "${transaction.name}"`, 'warn'); - } - - const activeSpan = this.activeSpans.get(span.id); - - if (activeSpan) { - activeSpan.endTime = getTimestamp(); - activeSpan.duration = activeSpan.endTime - activeSpan.startTime; - this.activeSpans.delete(span.id); - - // Update span in transaction with final data - Object.assign(span, activeSpan); - } - } - }); - - transaction.endTime = getTimestamp(); - transaction.duration = transaction.endTime - transaction.startTime; - this.activeTransactions.delete(transactionId); - - this.queueTransaction(transaction); - } - } - /** * Sends performance data to Hawk collector * @@ -285,7 +198,7 @@ export default class PerformanceMonitoring { */ public destroy(): void { // Finish any remaining transactions - this.activeTransactions.forEach((_, id) => this.finishTransaction(id)); + this.activeTransactions.forEach(transaction => transaction.finish()); // Clear any pending send timeout if (this.sendTimeout !== null) { @@ -303,3 +216,73 @@ export default class PerformanceMonitoring { } } } + +/** + * Class representing a span of work within a transaction + */ +export class Span { + public readonly id: string; + public readonly transactionId: string; + public readonly name: string; + public readonly startTime: number; + public endTime?: number; + public duration?: number; + public readonly metadata?: Record; + + constructor(data: Omit) { + Object.assign(this, data); + } + + public finish(): void { + this.endTime = getTimestamp(); + this.duration = this.endTime - this.startTime; + } +} + +/** + * Class representing a transaction that can contain multiple spans + */ +export class Transaction { + public readonly id: string; + public readonly traceId: string; + public readonly name: string; + public readonly startTime: number; + public endTime?: number; + public duration?: number; + public readonly tags: Record; + public readonly spans: Span[] = []; + + constructor( + data: Omit, + private readonly performance: PerformanceMonitoring + ) { + Object.assign(this, data); + } + + public startSpan(name: string, metadata?: Record): Span { + const data = { + id: id(), + transactionId: this.id, + name, + startTime: getTimestamp(), + metadata, + }; + + const span = new Span(data); + this.spans.push(span); + return span; + } + + public finish(): void { + // Finish all unfinished spans + this.spans.forEach(span => { + if (!span.endTime) { + span.finish(); + } + }); + + this.endTime = getTimestamp(); + this.duration = this.endTime - this.startTime; + this.performance.queueTransaction(this); + } +} diff --git a/src/types/span.ts b/src/types/span.ts deleted file mode 100644 index 338425c..0000000 --- a/src/types/span.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Interface for Span data - */ -export interface Span { - /** - * Unique identifier of the span - */ - id: string; - - /** - * ID of the parent transaction - */ - transactionId: string; - - /** - * Name of the span (e.g., 'db-query', 'http-request') - */ - name: string; - - /** - * Timestamp when the span started - */ - startTime: number; - - /** - * Timestamp when the span ended - */ - endTime?: number; - - /** - * Total duration of the span in milliseconds - */ - duration?: number; - - /** - * Additional context data for the span - */ - metadata?: Record; -} diff --git a/src/types/transaction.ts b/src/types/transaction.ts deleted file mode 100644 index a794a24..0000000 --- a/src/types/transaction.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Span } from './span'; - -/** - * Interface for Transaction data - */ -export interface Transaction { - /** - * Unique identifier of the transaction - */ - id: string; - - /** - * Identifier for grouping related transactions - */ - traceId: string; - - /** - * Name of the transaction (e.g., 'page-load', 'api-request') - */ - name: string; - - /** - * Timestamp when the transaction started - */ - startTime: number; - - /** - * Timestamp when the transaction ended - */ - endTime?: number; - - /** - * Total duration of the transaction in milliseconds - */ - duration?: number; - - /** - * Key-value pairs for additional transaction data - */ - tags: Record; - - /** - * List of spans associated with this transaction - */ - spans: Span[]; -} From b5d71c4e1e7f4ea84fff4b03867e76655b346441 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 19:56:10 +0300 Subject: [PATCH 07/37] Remove traceId --- README.md | 1 - src/modules/performance.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/README.md b/README.md index 46061c5..a3d60f0 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,6 @@ Finishes a transaction, calculates its duration, and sends the performance data ```typescript interface Transaction { id: string; - traceId: string; name: string; startTime: number; endTime?: number; diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 750a462..17c5826 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -163,7 +163,6 @@ export default class PerformanceMonitoring { public startTransaction(name: string, tags: Record = {}): Transaction { const data = { id: id(), - traceId: id(), name, startTime: getTimestamp(), tags, @@ -244,7 +243,6 @@ export class Span { */ export class Transaction { public readonly id: string; - public readonly traceId: string; public readonly name: string; public readonly startTime: number; public endTime?: number; From d708ebb1430d96d30fb45d610b9e4c30db83f236 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 20:05:31 +0300 Subject: [PATCH 08/37] feat(performance): add performance monitoring --- README.md | 15 ++++++- example/index.html | 1 + src/catcher.ts | 64 ++++++++++-------------------- src/types/hawk-initial-settings.ts | 5 +++ 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index a3d60f0..ed0bd74 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,15 @@ hawk.connectVue(Vue) ## Performance Monitoring +To enable performance monitoring, set `performance: true` in the HawkCatcher constructor: + +```typescript +const hawk = new HawkCatcher({ + token: 'INTEGRATION_TOKEN', + performance: true +}); +``` + Hawk JavaScript Catcher includes a Performance Monitoring API to track application performance metrics: ```typescript @@ -192,11 +201,13 @@ Features: - Throttled data sending to prevent server overload - Graceful cleanup on page unload/process exit +> Note: If performance monitoring is not enabled, `startTransaction()` will return undefined and log an error to the console. + ### API Reference #### startTransaction(name: string, tags?: Record): Transaction -Starts a new transaction. A transaction represents a high-level operation like a page load or an API request. +Starts a new transaction. A transaction represents a high-level operation like a page load or an API call. - `name`: Name of the transaction - `tags`: Optional key-value pairs for additional transaction data @@ -282,7 +293,7 @@ router.beforeEach((to, from, next) => { async function fetchUsers() { const transaction = hawk.startTransaction('fetch-users'); - const apiSpan = transaction.startSpan('api-call', { + const apiSpan = transaction.startSpan('GET /api/user', { url: '/api/users', method: 'GET' }); diff --git a/example/index.html b/example/index.html index abbb269..fbdc895 100644 --- a/example/index.html +++ b/example/index.html @@ -298,6 +298,7 @@

Test Vue integration: <test-component>

context: { rootContextSample: '12345' }, + performance: true })
@@ -260,7 +288,7 @@

Test Vue integration: <test-component>

el: tag, instance: new Editor(), classProto: Editor, - longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', + longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', } }, props: { @@ -301,7 +329,8 @@

Test Vue integration: <test-component>

performance: true }) - From a0d56bbadee6fd4943de8dba205b2483eca412f1 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 23:19:49 +0300 Subject: [PATCH 11/37] refactor(performance): simplify transaction queuing and remove unused send timeout logic --- src/modules/performance.ts | 60 +++++--------------------------------- 1 file changed, 8 insertions(+), 52 deletions(-) diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 95e457a..0e9b56b 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -138,16 +138,6 @@ export default class PerformanceMonitoring { */ private sendQueue: Transaction[] = []; - /** - * Timestamp of last send operation - */ - private lastSendTime = 0; - - /** - * Scheduled send timeout ID - */ - private sendTimeout: number | NodeJS.Timeout | null = null; - private readonly sampleRate: number; /** @@ -205,22 +195,12 @@ export default class PerformanceMonitoring { } /** - * Schedule sending of performance data with throttling + * Queue transaction for sending */ - private scheduleSend(): void { - if (this.sendTimeout !== null) { - return; - } - - const now = Date.now(); - const timeSinceLastSend = now - this.lastSendTime; - const delay = Math.max(0, THROTTLE_INTERVAL - timeSinceLastSend); - - const timer = isBrowser ? window.setTimeout : setTimeout; - - this.sendTimeout = timer(() => { - void this.processSendQueue(); - }, delay); + public queueTransaction(transaction: Transaction): void { + this.activeTransactions.delete(transaction.id); + this.sendQueue.push(transaction); + void this.processSendQueue(); } /** @@ -228,35 +208,21 @@ export default class PerformanceMonitoring { */ private async processSendQueue(): Promise { if (this.sendQueue.length === 0) { - this.sendTimeout = null; - return; } try { const transaction = this.sendQueue.shift()!; - await this.sendPerformanceData(transaction); - this.lastSendTime = Date.now(); } catch (error) { if (this.debug) { log('Failed to send performance data', 'error', error); } - } finally { - this.sendTimeout = null; - if (this.sendQueue.length > 0) { - this.scheduleSend(); - } } - } - /** - * Queue transaction for sending - */ - public queueTransaction(transaction: Transaction): void { - this.activeTransactions.delete(transaction.id); - this.sendQueue.push(transaction); - this.scheduleSend(); + if (this.sendQueue.length > 0) { + void this.processSendQueue(); + } } /** @@ -322,16 +288,6 @@ export default class PerformanceMonitoring { // Finish any remaining transactions this.activeTransactions.forEach(transaction => transaction.finish()); - // Clear any pending send timeout - if (this.sendTimeout !== null) { - if (isBrowser) { - window.clearTimeout(this.sendTimeout as number); - } else { - clearTimeout(this.sendTimeout as NodeJS.Timeout); - } - this.sendTimeout = null; - } - // Force send any remaining queued data if (this.sendQueue.length > 0) { void this.processSendQueue(); From 933f7f94b617e5b3373c4d123f53ce8a15a1f940 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 23:24:16 +0300 Subject: [PATCH 12/37] refactor(performance): update PerformancePayload interface to include spans and remove Transaction inheritance --- src/types/performance-message.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/types/performance-message.ts b/src/types/performance-message.ts index 1872531..83d1702 100644 --- a/src/types/performance-message.ts +++ b/src/types/performance-message.ts @@ -1,13 +1,18 @@ -import type { Transaction } from './transaction'; +import type { Transaction } from '../modules/performance'; import type { EncodedIntegrationToken } from '@hawk.so/types'; +import type { Span } from '../modules/performance'; /** * Interface for performance monitoring message payload */ -export interface PerformancePayload extends Transaction { - /** - * Version of the catcher that sent this message - */ +export interface PerformancePayload { + id: string; + name: string; + startTime: number; + endTime?: number; + duration?: number; + tags: Record; + spans: Span[]; catcherVersion: string; } From 6c544bd0f1542455cde1c709a1f78dbc04625f88 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 23:36:09 +0300 Subject: [PATCH 13/37] Lint --- src/catcher.ts | 51 ++++---- src/modules/performance.ts | 179 ++++++++++++++++++----------- src/types/hawk-initial-settings.ts | 1 + src/types/performance-message.ts | 1 - 4 files changed, 136 insertions(+), 96 deletions(-) diff --git a/src/catcher.ts b/src/catcher.ts index 1271ac1..5d9a8db 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -16,7 +16,8 @@ import type { JavaScriptCatcherIntegrations } from './types/integrations'; import { EventRejectedError } from './errors'; import type { HawkJavaScriptEvent } from './types'; import { isErrorProcessed, markErrorAsProcessed } from './utils/event'; -import PerformanceMonitoring, { Transaction } from './modules/performance'; +import type { Transaction } from './modules/performance'; +import PerformanceMonitoring from './modules/performance'; /** * Allow to use global VERSION, that will be overwritten by Webpack @@ -155,8 +156,8 @@ export default class Catcher { } if (settings.performance) { - const sampleRate = typeof settings.performance === 'object' ? - settings.performance.sampleRate : + const sampleRate = typeof settings.performance === 'object' ? + settings.performance.sampleRate : 1.0; this.performance = new PerformanceMonitoring( @@ -167,7 +168,6 @@ export default class Catcher { sampleRate ); } - } /** @@ -239,6 +239,28 @@ export default class Catcher { }); } + + /** + * Starts a new transaction + * + * @param name - Name of the transaction (e.g., 'page-load', 'api-request') + * @param tags - Key-value pairs for additional transaction data + */ + public startTransaction(name: string, tags: Record = {}): Transaction | undefined { + if (this.performance === null) { + console.error('Performance monitoring is not enabled. Please enable it by setting performance: true in the HawkCatcher constructor.'); + } + + return this.performance?.startTransaction(name, tags); + } + + /** + * Clean up resources + */ + public destroy(): void { + this.performance?.destroy(); + } + /** * Init global errors handler */ @@ -565,25 +587,4 @@ export default class Catcher { private appendIntegrationAddons(errorFormatted: CatcherMessage, integrationAddons: JavaScriptCatcherIntegrations): void { Object.assign(errorFormatted.payload.addons, integrationAddons); } - - /** - * Starts a new transaction - * - * @param name - Name of the transaction (e.g., 'page-load', 'api-request') - * @param tags - Key-value pairs for additional transaction data - */ - public startTransaction(name: string, tags: Record = {}): Transaction | undefined { - if (this.performance === null) { - console.error('Performance monitoring is not enabled. Please enable it by setting performance: true in the HawkCatcher constructor.'); - } - - return this.performance?.startTransaction(name, tags); - } - - /** - * Clean up resources - */ - public destroy(): void { - this.performance?.destroy(); - } } diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 0e9b56b..bda8c01 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -23,11 +23,6 @@ const getTimestamp = (): number => { return Number(process.hrtime.bigint() / BigInt(1_000_000)); }; -/** - * Minimum interval between performance data sends (in milliseconds) - */ -const THROTTLE_INTERVAL = 1000; - /** * Class representing a span of work within a transaction */ @@ -38,12 +33,20 @@ export class Span { public readonly startTime: number; public endTime?: number; public duration?: number; - public readonly metadata?: Record; + public readonly metadata?: Record; + /** + * Constructor for Span + * + * @param data - Data to initialize the span with. Contains id, transactionId, name, startTime, metadata + */ constructor(data: Omit) { Object.assign(this, data); } + /** + * + */ public finish(): void { this.endTime = getTimestamp(); this.duration = this.endTime - this.startTime; @@ -62,6 +65,11 @@ export class Transaction { public readonly tags: Record; public readonly spans: Span[] = []; + /** + * + * @param data + * @param performance + */ constructor( data: Omit, private readonly performance: PerformanceMonitoring @@ -69,7 +77,12 @@ export class Transaction { Object.assign(this, data); } - public startSpan(name: string, metadata?: Record): Span { + /** + * + * @param name + * @param metadata + */ + public startSpan(name: string, metadata?: Record): Span { const data = { id: id(), transactionId: this.id, @@ -79,10 +92,15 @@ export class Transaction { }; const span = new Span(data); + this.spans.push(span); + return span; } + /** + * + */ public finish(): void { // Finish all unfinished spans this.spans.forEach(span => { @@ -101,11 +119,23 @@ export class Transaction { * Class representing a sampled out transaction that won't be sent to server */ class SampledOutTransaction extends Transaction { + /** + * Constructor for SampledOutTransaction + * + * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags and spans + */ constructor(data: Omit) { - super(data, null as any); // performance не используется + super(data, null as unknown as PerformanceMonitoring); // performance не используется } - public startSpan(name: string, metadata?: Record): Span { + /** + * Start a new span within this sampled out transaction + * + * @param name - Name of the span + * @param metadata - Optional metadata to attach to the span + * @returns A new Span instance that won't be sent to server + */ + public startSpan(name: string, metadata?: Record): Span { const data = { id: id(), transactionId: this.id, @@ -115,10 +145,15 @@ class SampledOutTransaction extends Transaction { }; const span = new Span(data); + this.spans.push(span); + return span; } + /** + * + */ public finish(): void { // Do nothing - don't send to server } @@ -159,7 +194,7 @@ export default class PerformanceMonitoring { sampleRate = 1; } this.sampleRate = Math.max(0, Math.min(1, sampleRate)); - + if (isBrowser) { this.initBeforeUnloadHandler(); } else { @@ -167,6 +202,69 @@ export default class PerformanceMonitoring { } } + /** + * Queue transaction for sending + * + * @param transaction + */ + public queueTransaction(transaction: Transaction): void { + this.activeTransactions.delete(transaction.id); + this.sendQueue.push(transaction); + void this.processSendQueue(); + } + + + /** + * Starts a new transaction + * + * @param name - Transaction name + * @param tags - Optional tags for the transaction + * @returns Transaction object + */ + public startTransaction(name: string, tags: Record = {}): Transaction { + // Sample transactions based on rate + if (Math.random() > this.sampleRate) { + if (this.debug) { + log(`Transaction "${name}" was sampled out`, 'info'); + } + + return new SampledOutTransaction({ + id: id(), + name, + startTime: getTimestamp(), + tags, + spans: [], + }); + } + + const data = { + id: id(), + name, + startTime: getTimestamp(), + tags, + spans: [], + }; + + const transaction = new Transaction(data, this); + + this.activeTransactions.set(transaction.id, transaction); + + return transaction; + } + + /** + * Clean up resources and ensure all data is sent + */ + public destroy(): void { + // Finish any remaining transactions + this.activeTransactions.forEach(transaction => transaction.finish()); + + // Force send any remaining queued data + if (this.sendQueue.length > 0) { + void this.processSendQueue(); + } + } + /** * Initialize handler for browser page unload */ @@ -194,15 +292,6 @@ export default class PerformanceMonitoring { }); } - /** - * Queue transaction for sending - */ - public queueTransaction(transaction: Transaction): void { - this.activeTransactions.delete(transaction.id); - this.sendQueue.push(transaction); - void this.processSendQueue(); - } - /** * Process queued transactions */ @@ -213,6 +302,7 @@ export default class PerformanceMonitoring { try { const transaction = this.sendQueue.shift()!; + await this.sendPerformanceData(transaction); } catch (error) { if (this.debug) { @@ -225,44 +315,6 @@ export default class PerformanceMonitoring { } } - /** - * Starts a new transaction - * - * @param name - Transaction name - * @param tags - Optional tags for the transaction - * @returns Transaction object - */ - public startTransaction(name: string, tags: Record = {}): Transaction { - // Sample transactions based on rate - if (Math.random() > this.sampleRate) { - if (this.debug) { - log(`Transaction "${name}" was sampled out`, 'info'); - } - - return new SampledOutTransaction({ - id: id(), - name, - startTime: getTimestamp(), - tags, - spans: [] - }); - } - - const data = { - id: id(), - name, - startTime: getTimestamp(), - tags, - spans: [], - }; - - const transaction = new Transaction(data, this); - - this.activeTransactions.set(transaction.id, transaction); - - return transaction; - } - /** * Sends performance data to Hawk collector * @@ -280,17 +332,4 @@ export default class PerformanceMonitoring { await this.transport.send(performanceMessage); } - - /** - * Clean up resources and ensure all data is sent - */ - public destroy(): void { - // Finish any remaining transactions - this.activeTransactions.forEach(transaction => transaction.finish()); - - // Force send any remaining queued data - if (this.sendQueue.length > 0) { - void this.processSendQueue(); - } - } } diff --git a/src/types/hawk-initial-settings.ts b/src/types/hawk-initial-settings.ts index 44ab13a..a0bcd4a 100644 --- a/src/types/hawk-initial-settings.ts +++ b/src/types/hawk-initial-settings.ts @@ -83,6 +83,7 @@ export interface HawkInitialSettings { performance?: boolean | { /** * Sample rate for performance data (0.0 to 1.0) + * * @default 1.0 */ sampleRate: number; diff --git a/src/types/performance-message.ts b/src/types/performance-message.ts index 83d1702..f625864 100644 --- a/src/types/performance-message.ts +++ b/src/types/performance-message.ts @@ -1,4 +1,3 @@ -import type { Transaction } from '../modules/performance'; import type { EncodedIntegrationToken } from '@hawk.so/types'; import type { Span } from '../modules/performance'; From 8309ae341a87e28ba18faf02966bd09b953901ad Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 23:39:29 +0300 Subject: [PATCH 14/37] Fix --- example/index.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example/index.html b/example/index.html index cc3f4f8..6e4b3ed 100644 --- a/example/index.html +++ b/example/index.html @@ -223,7 +223,7 @@

Performance Monitoring Settings Demo

Test Vue integration: <test-component> button.disabled = false; }); - - updateSampleRate(0.5) From d611f1d58f70365c7cad2a2cd4eab6422bd2fb2c Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sat, 15 Mar 2025 23:50:41 +0300 Subject: [PATCH 15/37] Review --- src/modules/performance.ts | 35 +++++++++++++---------------------- src/utils/get-timestamp.ts | 16 ++++++++++++++++ src/utils/is-browser.ts | 4 ++++ 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 src/utils/get-timestamp.ts create mode 100644 src/utils/is-browser.ts diff --git a/src/modules/performance.ts b/src/modules/performance.ts index bda8c01..565adb8 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -2,26 +2,8 @@ import type { PerformanceMessage } from '../types/performance-message'; import { id } from '../utils/id'; import log from '../utils/log'; import type Socket from './socket'; - -/** - * Check if code is running in browser environment - */ -const isBrowser = typeof window !== 'undefined'; - -/** - * Get high-resolution timestamp in milliseconds - */ -const getTimestamp = (): number => { - if (isBrowser) { - return performance.now(); - } - - /** - * process.hrtime.bigint() returns nanoseconds - * Convert to milliseconds for consistency with browser - */ - return Number(process.hrtime.bigint() / BigInt(1_000_000)); -}; +import { isBrowser } from '../utils/is-browser'; +import { getTimestamp } from '../utils/get-timestamp'; /** * Class representing a span of work within a transaction @@ -164,7 +146,11 @@ class SampledOutTransaction extends Transaction { */ export default class PerformanceMonitoring { /** - * Active transactions map + * Map of active transactions by their ID + * Used to: + * - Track transactions that haven't been finished yet + * - Finish all active transactions on page unload/process exit + * - Prevent memory leaks by removing finished transactions */ private activeTransactions: Map = new Map(); @@ -173,6 +159,10 @@ export default class PerformanceMonitoring { */ private sendQueue: Transaction[] = []; + /** + * Sample rate for performance data + * Used to determine if a transaction should be sampled out + */ private readonly sampleRate: number; /** @@ -286,7 +276,8 @@ export default class PerformanceMonitoring { ['SIGINT', 'SIGTERM'].forEach(signal => { process.on(signal, () => { - this.activeTransactions.forEach(transaction => transaction.finish()); + // Prevent immediate exit + this.destroy(); process.exit(0); }); }); diff --git a/src/utils/get-timestamp.ts b/src/utils/get-timestamp.ts new file mode 100644 index 0000000..d4574b9 --- /dev/null +++ b/src/utils/get-timestamp.ts @@ -0,0 +1,16 @@ +import { isBrowser } from './is-browser'; + +/** + * Get high-resolution timestamp in milliseconds + */ +export const getTimestamp = (): number => { + if (isBrowser) { + return performance.now(); + } + + /** + * process.hrtime.bigint() returns nanoseconds + * Convert to milliseconds for consistency with browser + */ + return Number(process.hrtime.bigint() / BigInt(1_000_000)); +}; diff --git a/src/utils/is-browser.ts b/src/utils/is-browser.ts new file mode 100644 index 0000000..20732c6 --- /dev/null +++ b/src/utils/is-browser.ts @@ -0,0 +1,4 @@ +/** + * Check if code is running in browser environment + */ +export const isBrowser = typeof window !== 'undefined'; From 2459f5d6c995f217783431ccef38b575cf29dd0f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 00:12:22 +0300 Subject: [PATCH 16/37] feat(performance): introduce batch sending configuration and enhance performance monitoring capabilities --- README.md | 47 ++++++++++------------ src/catcher.ts | 11 ++++-- src/modules/performance.ts | 62 +++++++++++++++++++++++------- src/types/hawk-initial-settings.ts | 7 ++++ src/types/performance-message.ts | 2 +- 5 files changed, 85 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 43b88c9..cf5beec 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Initialization settings: | `disableGlobalErrorsHandling` | boolean | optional | Do not initialize global errors handling | | `disableVueErrorHandler` | boolean | optional | Do not initialize Vue errors handling | | `beforeSend` | function(event) => event | optional | This Method allows you to filter any data you don't want sending to Hawk | +| `performance` | boolean\|object | optional | Performance monitoring settings. When object, accepts:
- `sampleRate`: Sample rate (0.0 to 1.0, default: 1.0)
- `batchInterval`: Batch send interval in ms (default: 3000) | Other available [initial settings](types/hawk-initial-settings.d.ts) are described at the type definition. @@ -163,44 +164,38 @@ hawk.connectVue(Vue) ## Performance Monitoring -To enable performance monitoring, configure it in the HawkCatcher constructor: +The SDK can monitor performance of your application by tracking transactions and spans. -```typescript -const hawk = new HawkCatcher({ - token: 'INTEGRATION_TOKEN', - // Enable with default settings (100% sampling) - performance: true -}); +### Transaction Batching + +By default, transactions are collected and sent in batches every 3 seconds to reduce network overhead. +You can configure the batch interval using the `batchInterval` option: -// Or enable with custom sample rate +```js const hawk = new HawkCatcher({ token: 'INTEGRATION_TOKEN', performance: { - // Sample 20% of transactions - sampleRate: 0.2 + batchInterval: 5000 // Send batches every 5 seconds } }); ``` -Hawk JavaScript Catcher includes a Performance Monitoring API to track application performance metrics: - -```typescript -// Start a transaction -const transaction = hawk.startTransaction('page-load', { - page: '/home', - type: 'navigation' -}); +Transactions are automatically batched and sent: +- Every `batchInterval` milliseconds +- When the page is unloaded (in browser) +- When the process exits (in Node.js) -// Create spans within transaction -const span = transaction.startSpan('api-call', { - url: '/api/users' -}); +### Sampling -// Finish span when operation completes -span.finish(); +You can configure what percentage of transactions should be sent to Hawk using the `sampleRate` option: -// Finish transaction -transaction.finish(); +```typescript +const hawk = new HawkCatcher({ + token: 'INTEGRATION_TOKEN', + performance: { + sampleRate: 0.2 + } +}); ``` Features: diff --git a/src/catcher.ts b/src/catcher.ts index 5d9a8db..cae8ad6 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -156,16 +156,21 @@ export default class Catcher { } if (settings.performance) { - const sampleRate = typeof settings.performance === 'object' ? - settings.performance.sampleRate : + const sampleRate = typeof settings.performance === 'object' ? + settings.performance.sampleRate : 1.0; + const batchInterval = typeof settings.performance === 'object' ? + settings.performance.batchInterval : + undefined; + this.performance = new PerformanceMonitoring( this.transport, this.token, this.version, this.debug, - sampleRate + sampleRate, + batchInterval ); } } diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 565adb8..323773a 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -5,6 +5,11 @@ import type Socket from './socket'; import { isBrowser } from '../utils/is-browser'; import { getTimestamp } from '../utils/get-timestamp'; +/** + * Default interval between batch sends in milliseconds + */ +const DEFAULT_BATCH_INTERVAL = 3000; + /** * Class representing a span of work within a transaction */ @@ -141,10 +146,18 @@ class SampledOutTransaction extends Transaction { } } + + /** * Class for managing performance monitoring */ export default class PerformanceMonitoring { + + /** + * Timer for batch sending + */ + private batchTimeout: ReturnType | null = null; + /** * Map of active transactions by their ID * Used to: @@ -171,13 +184,15 @@ export default class PerformanceMonitoring { * @param version - Catcher version * @param debug - Debug mode flag * @param sampleRate - Sample rate for performance data (0.0 to 1.0) + * @param batchInterval - Interval between batch sends in milliseconds */ constructor( private readonly transport: Socket, private readonly token: string, private readonly version: string, private readonly debug: boolean = false, - sampleRate: number = 1.0 + sampleRate: number = 1.0, + private readonly batchInterval: number = DEFAULT_BATCH_INTERVAL, ) { if (sampleRate < 0 || sampleRate > 1) { console.error('Performance monitoring sample rate must be between 0 and 1'); @@ -190,6 +205,9 @@ export default class PerformanceMonitoring { } else { this.initProcessExitHandler(); } + + // Start batch sending timer + this.scheduleBatchSend(); } /** @@ -246,6 +264,14 @@ export default class PerformanceMonitoring { * Clean up resources and ensure all data is sent */ public destroy(): void { + // Clear batch sending timer + if (this.batchTimeout !== null) { + const clear = isBrowser ? window.clearInterval : clearInterval; + + clear(this.batchTimeout); + this.batchTimeout = null; + } + // Finish any remaining transactions this.activeTransactions.forEach(transaction => transaction.finish()); @@ -260,7 +286,7 @@ export default class PerformanceMonitoring { */ private initBeforeUnloadHandler(): void { window.addEventListener('beforeunload', () => { - // Finish any active transactions before unload + // Finish any active transactions this.activeTransactions.forEach(transaction => transaction.finish()); }); } @@ -284,41 +310,49 @@ export default class PerformanceMonitoring { } /** - * Process queued transactions + * Schedule periodic batch sending of transactions + */ + private scheduleBatchSend(): void { + const timer = isBrowser ? window.setInterval : setInterval; + this.batchTimeout = timer(() => void this.processSendQueue(), this.batchInterval); + } + + /** + * Process queued transactions in batch */ private async processSendQueue(): Promise { if (this.sendQueue.length === 0) { return; } - try { - const transaction = this.sendQueue.shift()!; + // Get all transactions from queue + const transactions = [...this.sendQueue]; + this.sendQueue = []; - await this.sendPerformanceData(transaction); + try { + await this.sendPerformanceData(transactions); } catch (error) { if (this.debug) { log('Failed to send performance data', 'error', error); + // Return failed transactions to queue + this.sendQueue.push(...transactions); } } - - if (this.sendQueue.length > 0) { - void this.processSendQueue(); - } } /** * Sends performance data to Hawk collector * - * @param transaction - Transaction data to send + * @param transactions - Array of transactions to send */ - private async sendPerformanceData(transaction: Transaction): Promise { + private async sendPerformanceData(transactions: Transaction[]): Promise { const performanceMessage: PerformanceMessage = { token: this.token, catcherType: 'performance', - payload: { + payload: transactions.map(transaction => ({ ...transaction, catcherVersion: this.version, - }, + })), }; await this.transport.send(performanceMessage); diff --git a/src/types/hawk-initial-settings.ts b/src/types/hawk-initial-settings.ts index a0bcd4a..8c66031 100644 --- a/src/types/hawk-initial-settings.ts +++ b/src/types/hawk-initial-settings.ts @@ -87,5 +87,12 @@ export interface HawkInitialSettings { * @default 1.0 */ sampleRate: number; + + /** + * Interval between batch sends in milliseconds + * + * @default 3000 + */ + batchInterval?: number; }; } diff --git a/src/types/performance-message.ts b/src/types/performance-message.ts index f625864..7faa15c 100644 --- a/src/types/performance-message.ts +++ b/src/types/performance-message.ts @@ -32,5 +32,5 @@ export interface PerformanceMessage { /** * Performance monitoring data */ - payload: PerformancePayload; + payload: PerformancePayload[]; } From c1639ca7b0d3508887dbeee8fb4c086e43f65f6b Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 00:29:18 +0300 Subject: [PATCH 17/37] feat(performance): add batch interval configuration and update UI for performance monitoring settings --- example/index.html | 62 ++++++++++++++++++++++++++++++++------ src/modules/performance.ts | 21 +++++++------ 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/example/index.html b/example/index.html index 6e4b3ed..d3be89f 100644 --- a/example/index.html +++ b/example/index.html @@ -126,6 +126,10 @@ margin-top: -5px; } + #sampleRate, #batchInterval, #generateTransactions { + margin-bottom: 10px; + } + .performance-demo button[disabled] .spinner { display: block; } @@ -218,7 +222,7 @@

Performance Monitoring Demo

Performance Monitoring Settings Demo

-
+
Performance Monitoring Settings Demo step="0.1" oninput="updateSampleRate(this.value)" > + + +
+
-
-
-
-
Sampled: 0 / Total: 0
+
+ Last batch sent: Never +
@@ -288,7 +302,7 @@

Test Vue integration: <test-component>

el: tag, instance: new Editor(), classProto: Editor, - longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', + longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', } }, props: { @@ -359,10 +373,27 @@

Test Vue integration: <test-component>

// Function to update sample rate window.updateSampleRate = (value) => { const rate = parseFloat(value); + const interval = parseInt(document.getElementById('batchInterval').value); + window.hawk = new HawkCatcher({ token: 'eyJpbnRlZ3JhdGlvbklkIjoiNWU5OTE1MzItZTdiYy00ZjA0LTliY2UtYmIzZmE5ZTUwMTg3Iiwic2VjcmV0IjoiMTBlMTA4MjQtZTcyNC00YWFkLTkwMDQtMzExYTU1OWMzZTIxIn0=', performance: { - sampleRate: rate + sampleRate: rate, + batchInterval: interval + } + }); + }; + + // Function to update batch interval + window.updateBatchInterval = (value) => { + const interval = parseInt(value); + const rate = parseFloat(document.getElementById('sampleRate').value); + + window.hawk = new HawkCatcher({ + token: 'eyJpbnRlZ3JhdGlvbklkIjoiNWU5OTE1MzItZTdiYy00ZjA0LTliY2UtYmIzZmE5ZTUwMTg3Iiwic2VjcmV0IjoiMTBlMTA4MjQtZTcyNC00YWFkLTkwMDQtMzExYTU1OWMzZTIxIn0=', + performance: { + sampleRate: rate, + batchInterval: interval } }); }; @@ -370,6 +401,7 @@

Test Vue integration: <test-component>

// Test sampling and throttling let totalTransactions = 0; let sampledTransactions = 0; + let lastBatchTime = null; document.getElementById('generateTransactions').addEventListener('click', async () => { const button = document.getElementById('generateTransactions'); @@ -379,7 +411,7 @@

Test Vue integration: <test-component>

totalTransactions = 0; sampledTransactions = 0; - // Generate 10 transactions rapidly + // Generate transactions rapidly for (let i = 0; i < 100; i++) { totalTransactions++; @@ -396,7 +428,7 @@

Test Vue integration: <test-component>

await new Promise(resolve => setTimeout(resolve, 50)); span2.finish(); - // Check if transaction was sampled (not a SampledOutTransaction) + // Check if transaction was sampled if (transaction.finish.toString().includes('queueTransaction')) { sampledTransactions++; } @@ -410,6 +442,18 @@

Test Vue integration: <test-component>

button.disabled = false; }); + + // Monitor WebSocket messages to detect batches + const originalSend = WebSocket.prototype.send; + WebSocket.prototype.send = function(data) { + const message = JSON.parse(data); + if (message.catcherType === 'performance') { + lastBatchTime = new Date(); + document.getElementById('batchStats').textContent = + `Last batch sent: ${lastBatchTime.toLocaleTimeString()} (${message.payload.length} transactions)`; + } + return originalSend.call(this, data); + }; diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 323773a..4921a43 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -32,7 +32,7 @@ export class Span { } /** - * + * Finishes the span by setting the end time and calculating duration */ public finish(): void { this.endTime = getTimestamp(); @@ -53,9 +53,10 @@ export class Transaction { public readonly spans: Span[] = []; /** - * - * @param data - * @param performance + * Constructor for Transaction + * + * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags + * @param performance - Reference to the PerformanceMonitoring instance that created this transaction */ constructor( data: Omit, @@ -146,8 +147,6 @@ class SampledOutTransaction extends Transaction { } } - - /** * Class for managing performance monitoring */ @@ -218,10 +217,8 @@ export default class PerformanceMonitoring { public queueTransaction(transaction: Transaction): void { this.activeTransactions.delete(transaction.id); this.sendQueue.push(transaction); - void this.processSendQueue(); } - /** * Starts a new transaction * @@ -230,6 +227,7 @@ export default class PerformanceMonitoring { * @returns Transaction object */ public startTransaction(name: string, tags: Record = {}): Transaction { + debugger // Sample transactions based on rate if (Math.random() > this.sampleRate) { if (this.debug) { @@ -314,6 +312,8 @@ export default class PerformanceMonitoring { */ private scheduleBatchSend(): void { const timer = isBrowser ? window.setInterval : setInterval; + + // Устанавливаем интервал для последующих отправок this.batchTimeout = timer(() => void this.processSendQueue(), this.batchInterval); } @@ -332,10 +332,11 @@ export default class PerformanceMonitoring { try { await this.sendPerformanceData(transactions); } catch (error) { + // Return failed transactions to queue + this.sendQueue.push(...transactions); + if (this.debug) { log('Failed to send performance data', 'error', error); - // Return failed transactions to queue - this.sendQueue.push(...transactions); } } } From c8b37bcc7c5ce0af545486b1090cc77ccf89d32b Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 00:38:09 +0300 Subject: [PATCH 18/37] style: clean up code formatting and remove unnecessary whitespace in performance and utility modules --- src/catcher.ts | 4 ++-- src/modules/performance.ts | 14 +++++++------- src/utils/get-timestamp.ts | 2 +- src/utils/is-browser.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/catcher.ts b/src/catcher.ts index cae8ad6..55dceb0 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -156,8 +156,8 @@ export default class Catcher { } if (settings.performance) { - const sampleRate = typeof settings.performance === 'object' ? - settings.performance.sampleRate : + const sampleRate = typeof settings.performance === 'object' ? + settings.performance.sampleRate : 1.0; const batchInterval = typeof settings.performance === 'object' ? diff --git a/src/modules/performance.ts b/src/modules/performance.ts index 4921a43..dd50e61 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -54,7 +54,7 @@ export class Transaction { /** * Constructor for Transaction - * + * * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags * @param performance - Reference to the PerformanceMonitoring instance that created this transaction */ @@ -151,7 +151,6 @@ class SampledOutTransaction extends Transaction { * Class for managing performance monitoring */ export default class PerformanceMonitoring { - /** * Timer for batch sending */ @@ -191,7 +190,7 @@ export default class PerformanceMonitoring { private readonly version: string, private readonly debug: boolean = false, sampleRate: number = 1.0, - private readonly batchInterval: number = DEFAULT_BATCH_INTERVAL, + private readonly batchInterval: number = DEFAULT_BATCH_INTERVAL ) { if (sampleRate < 0 || sampleRate > 1) { console.error('Performance monitoring sample rate must be between 0 and 1'); @@ -227,7 +226,7 @@ export default class PerformanceMonitoring { * @returns Transaction object */ public startTransaction(name: string, tags: Record = {}): Transaction { - debugger + debugger; // Sample transactions based on rate if (Math.random() > this.sampleRate) { if (this.debug) { @@ -265,7 +264,7 @@ export default class PerformanceMonitoring { // Clear batch sending timer if (this.batchTimeout !== null) { const clear = isBrowser ? window.clearInterval : clearInterval; - + clear(this.batchTimeout); this.batchTimeout = null; } @@ -326,7 +325,8 @@ export default class PerformanceMonitoring { } // Get all transactions from queue - const transactions = [...this.sendQueue]; + const transactions = [ ...this.sendQueue ]; + this.sendQueue = []; try { @@ -334,7 +334,7 @@ export default class PerformanceMonitoring { } catch (error) { // Return failed transactions to queue this.sendQueue.push(...transactions); - + if (this.debug) { log('Failed to send performance data', 'error', error); } diff --git a/src/utils/get-timestamp.ts b/src/utils/get-timestamp.ts index d4574b9..d18891f 100644 --- a/src/utils/get-timestamp.ts +++ b/src/utils/get-timestamp.ts @@ -13,4 +13,4 @@ export const getTimestamp = (): number => { * Convert to milliseconds for consistency with browser */ return Number(process.hrtime.bigint() / BigInt(1_000_000)); -}; +}; diff --git a/src/utils/is-browser.ts b/src/utils/is-browser.ts index 20732c6..b0b5638 100644 --- a/src/utils/is-browser.ts +++ b/src/utils/is-browser.ts @@ -1,4 +1,4 @@ /** * Check if code is running in browser environment */ -export const isBrowser = typeof window !== 'undefined'; +export const isBrowser = typeof window !== 'undefined'; From 1e57084e1fdc12793d4873d327ba52bc32456216 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 00:48:43 +0300 Subject: [PATCH 19/37] chore(performance): remove debugger statement from startTransaction method --- src/modules/performance.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/performance.ts b/src/modules/performance.ts index dd50e61..3ac612d 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance.ts @@ -226,7 +226,6 @@ export default class PerformanceMonitoring { * @returns Transaction object */ public startTransaction(name: string, tags: Record = {}): Transaction { - debugger; // Sample transactions based on rate if (Math.random() > this.sampleRate) { if (this.debug) { From 8e8d0f7f548a4e26cf3a3f1a736824091ecb7e7a Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 11:55:33 +0300 Subject: [PATCH 20/37] Split --- src/catcher.ts | 2 +- .../{performance.ts => performance/index.ts} | 150 +----------------- src/modules/performance/span.ts | 31 ++++ src/modules/performance/transaction.ts | 136 ++++++++++++++++ 4 files changed, 176 insertions(+), 143 deletions(-) rename src/modules/{performance.ts => performance/index.ts} (61%) create mode 100644 src/modules/performance/span.ts create mode 100644 src/modules/performance/transaction.ts diff --git a/src/catcher.ts b/src/catcher.ts index 55dceb0..6fa28f6 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -16,8 +16,8 @@ import type { JavaScriptCatcherIntegrations } from './types/integrations'; import { EventRejectedError } from './errors'; import type { HawkJavaScriptEvent } from './types'; import { isErrorProcessed, markErrorAsProcessed } from './utils/event'; -import type { Transaction } from './modules/performance'; import PerformanceMonitoring from './modules/performance'; +import { Transaction } from './modules/performance/transaction'; /** * Allow to use global VERSION, that will be overwritten by Webpack diff --git a/src/modules/performance.ts b/src/modules/performance/index.ts similarity index 61% rename from src/modules/performance.ts rename to src/modules/performance/index.ts index 3ac612d..8873c20 100644 --- a/src/modules/performance.ts +++ b/src/modules/performance/index.ts @@ -1,151 +1,17 @@ -import type { PerformanceMessage } from '../types/performance-message'; -import { id } from '../utils/id'; -import log from '../utils/log'; -import type Socket from './socket'; -import { isBrowser } from '../utils/is-browser'; -import { getTimestamp } from '../utils/get-timestamp'; +import type { PerformanceMessage } from '../../types/performance-message'; +import { id } from '../../utils/id'; +import log from '../../utils/log'; +import type Socket from '../socket'; +import { isBrowser } from '../../utils/is-browser'; +import { getTimestamp } from '../../utils/get-timestamp'; +import { Transaction, SampledOutTransaction } from './transaction'; /** * Default interval between batch sends in milliseconds */ const DEFAULT_BATCH_INTERVAL = 3000; -/** - * Class representing a span of work within a transaction - */ -export class Span { - public readonly id: string; - public readonly transactionId: string; - public readonly name: string; - public readonly startTime: number; - public endTime?: number; - public duration?: number; - public readonly metadata?: Record; - - /** - * Constructor for Span - * - * @param data - Data to initialize the span with. Contains id, transactionId, name, startTime, metadata - */ - constructor(data: Omit) { - Object.assign(this, data); - } - - /** - * Finishes the span by setting the end time and calculating duration - */ - public finish(): void { - this.endTime = getTimestamp(); - this.duration = this.endTime - this.startTime; - } -} - -/** - * Class representing a transaction that can contain multiple spans - */ -export class Transaction { - public readonly id: string; - public readonly name: string; - public readonly startTime: number; - public endTime?: number; - public duration?: number; - public readonly tags: Record; - public readonly spans: Span[] = []; - - /** - * Constructor for Transaction - * - * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags - * @param performance - Reference to the PerformanceMonitoring instance that created this transaction - */ - constructor( - data: Omit, - private readonly performance: PerformanceMonitoring - ) { - Object.assign(this, data); - } - - /** - * - * @param name - * @param metadata - */ - public startSpan(name: string, metadata?: Record): Span { - const data = { - id: id(), - transactionId: this.id, - name, - startTime: getTimestamp(), - metadata, - }; - - const span = new Span(data); - - this.spans.push(span); - - return span; - } - /** - * - */ - public finish(): void { - // Finish all unfinished spans - this.spans.forEach(span => { - if (!span.endTime) { - span.finish(); - } - }); - - this.endTime = getTimestamp(); - this.duration = this.endTime - this.startTime; - this.performance.queueTransaction(this); - } -} - -/** - * Class representing a sampled out transaction that won't be sent to server - */ -class SampledOutTransaction extends Transaction { - /** - * Constructor for SampledOutTransaction - * - * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags and spans - */ - constructor(data: Omit) { - super(data, null as unknown as PerformanceMonitoring); // performance не используется - } - - /** - * Start a new span within this sampled out transaction - * - * @param name - Name of the span - * @param metadata - Optional metadata to attach to the span - * @returns A new Span instance that won't be sent to server - */ - public startSpan(name: string, metadata?: Record): Span { - const data = { - id: id(), - transactionId: this.id, - name, - startTime: getTimestamp(), - metadata, - }; - - const span = new Span(data); - - this.spans.push(span); - - return span; - } - - /** - * - */ - public finish(): void { - // Do nothing - don't send to server - } -} /** * Class for managing performance monitoring @@ -350,7 +216,7 @@ export default class PerformanceMonitoring { token: this.token, catcherType: 'performance', payload: transactions.map(transaction => ({ - ...transaction, + ...transaction.getData(), catcherVersion: this.version, })), }; diff --git a/src/modules/performance/span.ts b/src/modules/performance/span.ts new file mode 100644 index 0000000..c4aa04d --- /dev/null +++ b/src/modules/performance/span.ts @@ -0,0 +1,31 @@ +import { getTimestamp } from "../../utils/get-timestamp"; + +/** + * Class representing a span of work within a transaction + */ +export class Span { + public readonly id: string; + public readonly transactionId: string; + public readonly name: string; + public readonly startTime: number; + public endTime?: number; + public duration?: number; + public readonly metadata?: Record; + + /** + * Constructor for Span + * + * @param data - Data to initialize the span with. Contains id, transactionId, name, startTime, metadata + */ + constructor(data: Omit) { + Object.assign(this, data); + } + + /** + * Finishes the span by setting the end time and calculating duration + */ + public finish(): void { + this.endTime = getTimestamp(); + this.duration = this.endTime - this.startTime; + } +} diff --git a/src/modules/performance/transaction.ts b/src/modules/performance/transaction.ts new file mode 100644 index 0000000..49c8d83 --- /dev/null +++ b/src/modules/performance/transaction.ts @@ -0,0 +1,136 @@ +import { getTimestamp } from "../../utils/get-timestamp"; +import PerformanceMonitoring from "."; +import { Span } from "./span"; +import { id } from "../../utils/id"; + +interface TransactionData { + id: string; + name: string; + startTime: number; + endTime?: number; + duration?: number; + tags: Record; + spans: Span[]; +} + +/** + * Class representing a transaction that can contain multiple spans + */ +export class Transaction { + public readonly id: string; + public readonly name: string; + public readonly startTime: number; + public endTime?: number; + public duration?: number; + public readonly tags: Record; + public readonly spans: Span[] = []; + + /** + * Constructor for Transaction + * + * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags + * @param performance - Reference to the PerformanceMonitoring instance that created this transaction + */ + constructor( + data: TransactionData, + private readonly performance: PerformanceMonitoring + ) { + this.id = data.id; + this.name = data.name; + this.startTime = data.startTime; + this.endTime = data.endTime; + this.duration = data.duration; + this.tags = data.tags; + this.spans = data.spans; + } + + /** + * + * @param name + * @param metadata + */ + public startSpan(name: string, metadata?: Record): Span { + const data = { + id: id(), + transactionId: this.id, + name, + startTime: getTimestamp(), + metadata, + }; + + const span = new Span(data); + + this.spans.push(span); + + return span; + } + + /** + * + */ + public finish(): void { + // Finish all unfinished spans + this.spans.forEach(span => { + if (!span.endTime) { + span.finish(); + } + }); + + this.endTime = getTimestamp(); + this.duration = this.endTime - this.startTime; + this.performance.queueTransaction(this); + } + + /** + * Returns transaction data that should be sent to server + */ + public getData(): TransactionData { + const { performance, ...data } = this; + + return data; + } +} + +/** + * Class representing a sampled out transaction that won't be sent to server + */ +export class SampledOutTransaction extends Transaction { + /** + * Constructor for SampledOutTransaction + * + * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags and spans + */ + constructor(data: TransactionData) { + super(data, null as unknown as PerformanceMonitoring); // performance не используется + } + + /** + * Start a new span within this sampled out transaction + * + * @param name - Name of the span + * @param metadata - Optional metadata to attach to the span + * @returns A new Span instance that won't be sent to server + */ + public startSpan(name: string, metadata?: Record): Span { + const data = { + id: id(), + transactionId: this.id, + name, + startTime: getTimestamp(), + metadata, + }; + + const span = new Span(data); + + this.spans.push(span); + + return span; + } + + /** + * + */ + public finish(): void { + // Do nothing - don't send to server + } +} From efa12f9d19bc92df62ba6b977d57d7253d9ce22f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 11:57:46 +0300 Subject: [PATCH 21/37] Review --- src/catcher.ts | 2 +- src/modules/performance/index.ts | 27 +++++++++++--------------- src/modules/performance/transaction.ts | 2 +- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/catcher.ts b/src/catcher.ts index 6fa28f6..154b63c 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -253,7 +253,7 @@ export default class Catcher { */ public startTransaction(name: string, tags: Record = {}): Transaction | undefined { if (this.performance === null) { - console.error('Performance monitoring is not enabled. Please enable it by setting performance: true in the HawkCatcher constructor.'); + console.error('Hawk: can not start transaction. Performance monitoring is not enabled. Please enable it by setting performance: true in the HawkCatcher constructor.'); } return this.performance?.startTransaction(name, tags); diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index 8873c20..1f65644 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -92,21 +92,6 @@ export default class PerformanceMonitoring { * @returns Transaction object */ public startTransaction(name: string, tags: Record = {}): Transaction { - // Sample transactions based on rate - if (Math.random() > this.sampleRate) { - if (this.debug) { - log(`Transaction "${name}" was sampled out`, 'info'); - } - - return new SampledOutTransaction({ - id: id(), - name, - startTime: getTimestamp(), - tags, - spans: [], - }); - } - const data = { id: id(), name, @@ -115,8 +100,18 @@ export default class PerformanceMonitoring { spans: [], }; - const transaction = new Transaction(data, this); + // Sample transactions based on rate + if (Math.random() > this.sampleRate) { + if (this.debug) { + log(`Transaction "${name}" was sampled out`, 'info'); + } + + return new SampledOutTransaction(data); + } + + const transaction = new Transaction(data, this); + this.activeTransactions.set(transaction.id, transaction); return transaction; diff --git a/src/modules/performance/transaction.ts b/src/modules/performance/transaction.ts index 49c8d83..c9c3b03 100644 --- a/src/modules/performance/transaction.ts +++ b/src/modules/performance/transaction.ts @@ -101,7 +101,7 @@ export class SampledOutTransaction extends Transaction { * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags and spans */ constructor(data: TransactionData) { - super(data, null as unknown as PerformanceMonitoring); // performance не используется + super(data, null as unknown as PerformanceMonitoring); } /** From 677fdafddc3ac536bc694f5809e34a50d4c8f87e Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 12:23:28 +0300 Subject: [PATCH 22/37] feat(performance): update HawkCatcher initialization and enhance performance monitoring structure --- README.md | 22 ++++------------- src/catcher.ts | 4 +-- src/modules/performance/index.ts | 11 +++------ src/modules/performance/transaction.ts | 34 ++++++-------------------- src/types/performance-message.ts | 20 +++------------ src/utils/get-timestamp.ts | 2 +- 6 files changed, 23 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index cf5beec..7795c72 100644 --- a/README.md +++ b/README.md @@ -272,11 +272,14 @@ interface Span { #### Measuring Route Changes in Vue.js ```javascript -import { Catcher } from '@hawk.so/javascript'; +import { HawkCatcher } from '@hawk.so/javascript'; import Vue from 'vue'; import Router from 'vue-router'; -const hawk = new Catcher('your-integration-token'); +const hawk = new HawkCatcher({ + token: 'INTEGRATION_TOKEN', + performance: true +}); router.beforeEach((to, from, next) => { const transaction = hawk.startTransaction('route-change', { @@ -319,18 +322,3 @@ async function fetchUsers() { } } ``` - -### Configuration - -- `token`: Your project's Integration Token -- `release`: Unique identifier of the release. Used for source map consuming -- `user`: Current authenticated user -- `context`: Any data you want to pass with every message. Has limitation of length. -- `vue`: Pass Vue constructor to set up the [Vue integration](#integrate-to-vue-application) -- `disableGlobalErrorsHandling`: Do not initialize global errors handling -- `disableVueErrorHandler`: Do not initialize Vue errors handling -- `beforeSend`: This Method allows you to filter any data you don't want sending to Hawk -- `performance`: Enable/disable performance monitoring - - `true`: Enable with 100% sampling - - `{sampleRate: number}`: Enable with custom sampling rate (0.0 to 1.0) - - `false` or `undefined`: Disable performance monitoring diff --git a/src/catcher.ts b/src/catcher.ts index 154b63c..c4eb1f0 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -156,11 +156,11 @@ export default class Catcher { } if (settings.performance) { - const sampleRate = typeof settings.performance === 'object' ? + const sampleRate = typeof settings.performance === 'object' && typeof settings.performance.sampleRate === 'number' ? settings.performance.sampleRate : 1.0; - const batchInterval = typeof settings.performance === 'object' ? + const batchInterval = typeof settings.performance === 'object' && typeof settings.performance.batchInterval === 'number' ? settings.performance.batchInterval : undefined; diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index 1f65644..8463be2 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -170,10 +170,8 @@ export default class PerformanceMonitoring { * Schedule periodic batch sending of transactions */ private scheduleBatchSend(): void { - const timer = isBrowser ? window.setInterval : setInterval; - // Устанавливаем интервал для последующих отправок - this.batchTimeout = timer(() => void this.processSendQueue(), this.batchInterval); + this.batchTimeout = setInterval(() => void this.processSendQueue(), this.batchInterval); } /** @@ -210,10 +208,9 @@ export default class PerformanceMonitoring { const performanceMessage: PerformanceMessage = { token: this.token, catcherType: 'performance', - payload: transactions.map(transaction => ({ - ...transaction.getData(), - catcherVersion: this.version, - })), + payload: { + transactions: transactions.map(transaction => transaction.getData()) + }, }; await this.transport.send(performanceMessage); diff --git a/src/modules/performance/transaction.ts b/src/modules/performance/transaction.ts index c9c3b03..1446ed7 100644 --- a/src/modules/performance/transaction.ts +++ b/src/modules/performance/transaction.ts @@ -3,7 +3,7 @@ import PerformanceMonitoring from "."; import { Span } from "./span"; import { id } from "../../utils/id"; -interface TransactionData { +export interface TransactionData { id: string; name: string; startTime: number; @@ -45,9 +45,11 @@ export class Transaction { } /** - * - * @param name - * @param metadata + * Starts a new span within this transaction + * + * @param name - Name of the span + * @param metadata - Optional metadata to attach to the span + * @returns New span instance */ public startSpan(name: string, metadata?: Record): Span { const data = { @@ -104,31 +106,9 @@ export class SampledOutTransaction extends Transaction { super(data, null as unknown as PerformanceMonitoring); } - /** - * Start a new span within this sampled out transaction - * - * @param name - Name of the span - * @param metadata - Optional metadata to attach to the span - * @returns A new Span instance that won't be sent to server - */ - public startSpan(name: string, metadata?: Record): Span { - const data = { - id: id(), - transactionId: this.id, - name, - startTime: getTimestamp(), - metadata, - }; - - const span = new Span(data); - - this.spans.push(span); - - return span; - } /** - * + * Finishes the transaction but does not send it to server since it was sampled out */ public finish(): void { // Do nothing - don't send to server diff --git a/src/types/performance-message.ts b/src/types/performance-message.ts index 7faa15c..487ef6c 100644 --- a/src/types/performance-message.ts +++ b/src/types/performance-message.ts @@ -1,19 +1,5 @@ import type { EncodedIntegrationToken } from '@hawk.so/types'; -import type { Span } from '../modules/performance'; - -/** - * Interface for performance monitoring message payload - */ -export interface PerformancePayload { - id: string; - name: string; - startTime: number; - endTime?: number; - duration?: number; - tags: Record; - spans: Span[]; - catcherVersion: string; -} +import { TransactionData } from 'src/modules/performance/transaction'; /** * Interface for performance monitoring message @@ -32,5 +18,7 @@ export interface PerformanceMessage { /** * Performance monitoring data */ - payload: PerformancePayload[]; + payload: { + transactions: TransactionData[]; + }; } diff --git a/src/utils/get-timestamp.ts b/src/utils/get-timestamp.ts index d18891f..6f64487 100644 --- a/src/utils/get-timestamp.ts +++ b/src/utils/get-timestamp.ts @@ -3,7 +3,7 @@ import { isBrowser } from './is-browser'; /** * Get high-resolution timestamp in milliseconds */ -export const getTimestamp = (): number => { +export function getTimestamp(): number { if (isBrowser) { return performance.now(); } From 1f1e7586dac70eed5b3456c84b6edeb2fa484d54 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 12:30:37 +0300 Subject: [PATCH 23/37] fix(performance): correct transaction length reference and update batch sending method to use setTimeout --- example/index.html | 2 +- src/modules/performance/index.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/example/index.html b/example/index.html index d3be89f..87faebf 100644 --- a/example/index.html +++ b/example/index.html @@ -450,7 +450,7 @@

Test Vue integration: <test-component>

if (message.catcherType === 'performance') { lastBatchTime = new Date(); document.getElementById('batchStats').textContent = - `Last batch sent: ${lastBatchTime.toLocaleTimeString()} (${message.payload.length} transactions)`; + `Last batch sent: ${lastBatchTime.toLocaleTimeString()} (${message.payload.transactions.length} transactions)`; } return originalSend.call(this, data); }; diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index 8463be2..69daab0 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -123,7 +123,7 @@ export default class PerformanceMonitoring { public destroy(): void { // Clear batch sending timer if (this.batchTimeout !== null) { - const clear = isBrowser ? window.clearInterval : clearInterval; + const clear = isBrowser ? window.clearTimeout : clearTimeout; clear(this.batchTimeout); this.batchTimeout = null; @@ -170,8 +170,11 @@ export default class PerformanceMonitoring { * Schedule periodic batch sending of transactions */ private scheduleBatchSend(): void { - // Устанавливаем интервал для последующих отправок - this.batchTimeout = setInterval(() => void this.processSendQueue(), this.batchInterval); + this.batchTimeout = setTimeout(() => { + void this.processSendQueue(); + + this.scheduleBatchSend(); // Schedule next batch + }, this.batchInterval); } /** From 93fd56c2aff1eb281f04022b88f933f114313303 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 15:22:10 +0300 Subject: [PATCH 24/37] Add doc --- docs/performance-monitoring.md | 101 +++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/performance-monitoring.md diff --git a/docs/performance-monitoring.md b/docs/performance-monitoring.md new file mode 100644 index 0000000..d35daea --- /dev/null +++ b/docs/performance-monitoring.md @@ -0,0 +1,101 @@ +# Performance Monitoring + +## Data types + +### Transaction +| Field | Type | Description | +|-------|------|-------------| +| id | string | Unique identifier of the transaction | +| type | string | Type of transaction sampling ('default' or 'critical'). See [Sampling](#sampling) for details | +| name | string | Name of the transaction | +| startTime | number | Timestamp when transaction started | +| endTime | number | Timestamp when transaction ended | +| duration | number | Total duration of transaction in milliseconds | +| finishStatus | string | Status when transaction finished. 'success' (default) or 'failure'. See [Transaction Completion](#2-transaction-completion) | +| spans | Span[] | Array of [spans](#span) associated with this transaction | +| tags | object | Additional context data attached to transaction | + +### AggregatedTransaction +| Field | Type | Description | +|-------|------|-------------| +| id | string | Unique identifier of the transaction | +| name | string | Name of the transaction | +| avgStartTime | number | Average timestamp when transaction started | +| minStartTime | number | Minimum timestamp when transaction started | +| maxEndTime | number | Maximum timestamp when transaction ended | +| p50duration | number | 50th percentile (median) duration of transaction in milliseconds | +| p95duration | number | 95th percentile duration of transaction in milliseconds | +| maxDuration | number | Maximum duration of transaction in milliseconds | + +### Span +| Field | Type | Description | +|-------|------|-------------| +| id | string | Unique identifier of the span | +| name | string | Name of the span | +| startTime | number | Timestamp when span started | +| endTime | number | Timestamp when span ended | +| duration | number | Total duration of span in milliseconds | + +### AggregatedSpan +See [Transaction Aggregation](#transaction-aggregation) for details on how spans are aggregated. + +| Field | Type | Description | +|-------|------|-------------| +| id | string | Unique identifier of the span | +| name | string | Name of the span | +| avgStartTime | number | Average timestamp when span started | +| minStartTime | number | Minimum timestamp when span started | +| maxEndTime | number | Maximum timestamp when span ended | +| p50duration | number | 50th percentile (median) duration of span in milliseconds | +| p95duration | number | 95th percentile duration of span in milliseconds | +| maxDuration | number | Maximum duration of span in milliseconds | + + +## Transaction Lifecycle + +### 1. Transaction Creation +When creating a transaction, you can specify its type: +- 'critical' - important transactions that are always sent to the server +- 'default' - regular transactions that go through the [sampling process](#sampling) + +### 2. Transaction Completion +When completing a transaction: +1. A finish status is specified (finishStatus): + - 'success' (default) - successful completion + - 'failure' - completion with error (such transactions are always sent to the server) + +2. The transaction duration is checked: + - If thresholdMs parameter is specified and the transaction duration is less than this value, the transaction is discarded + - Otherwise, the transaction goes through the [sampling process](#sampling) + +3. After successful sampling, the transaction is added to the list for sending + +### 3. Sending Transactions +- When the first transaction is added to the list, a timer starts +- When the timer expires: + 1. All collected transactions are [aggregated](#transaction-aggregation) + 2. Aggregated data is sent to the server + 3. The transaction list is cleared + +## Sampling +- The probability of sending transactions is configured through the `performance.sampleRate` parameter (value from 0 to 1) +- Only transactions of type 'default' with finish status 'success' are subject to sampling +- Sampling process: + 1. A random number between 0 and 1 is generated for each transaction + 2. If the number is less than or equal to sampleRate, the transaction is sent + +## Transaction Aggregation +1. [Transactions](#transaction) are grouped by name (name field) +2. For each group, statistical indicators are calculated: + - minStartTime - earliest start time + - maxEndTime - latest end time + - p50duration - median duration (50th percentile) + - p95duration - 95th percentile duration + - maxDuration - maximum duration + +3. Based on this data, [AggregatedTransaction](#aggregatedtransaction) objects are created + +4. For each aggregated transaction: + - [Spans](#span) are grouped by name + - The same statistical indicators are calculated for each span group + - [AggregatedSpan](#aggregatedspan) objects are created From 846e8643ec0220cdb4e1ba3e26b10a9206538078 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 16 Mar 2025 15:49:21 +0300 Subject: [PATCH 25/37] Update performance-monitoring.md --- docs/performance-monitoring.md | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/performance-monitoring.md b/docs/performance-monitoring.md index d35daea..d89dd2a 100644 --- a/docs/performance-monitoring.md +++ b/docs/performance-monitoring.md @@ -6,19 +6,18 @@ | Field | Type | Description | |-------|------|-------------| | id | string | Unique identifier of the transaction | -| type | string | Type of transaction sampling ('default' or 'critical'). See [Sampling](#sampling) for details | +| severity | string | Type of transaction sampling ('default' or 'critical'). See [Sampling](#sampling) for details | | name | string | Name of the transaction | | startTime | number | Timestamp when transaction started | | endTime | number | Timestamp when transaction ended | | duration | number | Total duration of transaction in milliseconds | | finishStatus | string | Status when transaction finished. 'success' (default) or 'failure'. See [Transaction Completion](#2-transaction-completion) | | spans | Span[] | Array of [spans](#span) associated with this transaction | -| tags | object | Additional context data attached to transaction | ### AggregatedTransaction | Field | Type | Description | |-------|------|-------------| -| id | string | Unique identifier of the transaction | +| aggregationId | string | Identifier of the aggregation | | name | string | Name of the transaction | | avgStartTime | number | Average timestamp when transaction started | | minStartTime | number | Minimum timestamp when transaction started | @@ -26,6 +25,9 @@ | p50duration | number | 50th percentile (median) duration of transaction in milliseconds | | p95duration | number | 95th percentile duration of transaction in milliseconds | | maxDuration | number | Maximum duration of transaction in milliseconds | +| count | number | how many transactions aggregated | +| aggregatedSpans | AggregatedSpan[] | List of spans in transactions | + ### Span | Field | Type | Description | @@ -41,36 +43,41 @@ See [Transaction Aggregation](#transaction-aggregation) for details on how spans | Field | Type | Description | |-------|------|-------------| -| id | string | Unique identifier of the span | +| aggregationId | string | Unique identifier of the span aggregation | | name | string | Name of the span | -| avgStartTime | number | Average timestamp when span started | | minStartTime | number | Minimum timestamp when span started | | maxEndTime | number | Maximum timestamp when span ended | | p50duration | number | 50th percentile (median) duration of span in milliseconds | | p95duration | number | 95th percentile duration of span in milliseconds | | maxDuration | number | Maximum duration of span in milliseconds | - ## Transaction Lifecycle ### 1. Transaction Creation + When creating a transaction, you can specify its type: + - 'critical' - important transactions that are always sent to the server - 'default' - regular transactions that go through the [sampling process](#sampling) ### 2. Transaction Completion + When completing a transaction: + 1. A finish status is specified (finishStatus): - 'success' (default) - successful completion - 'failure' - completion with error (such transactions are always sent to the server) 2. The transaction duration is checked: - - If thresholdMs parameter is specified and the transaction duration is less than this value, the transaction is discarded + - If `thresholdMs` parameter is specified and the transaction duration is less than this value, the transaction is discarded + - Default `thresholdMs` is 20ms + - `finishStatus` "failure" has a priority over `thresholdMs` - Otherwise, the transaction goes through the [sampling process](#sampling) -3. After successful sampling, the transaction is added to the list for sending +3. After successful sampling, the transaction is added to the list for sending ### 3. Sending Transactions + - When the first transaction is added to the list, a timer starts - When the timer expires: 1. All collected transactions are [aggregated](#transaction-aggregation) @@ -78,6 +85,7 @@ When completing a transaction: 3. The transaction list is cleared ## Sampling + - The probability of sending transactions is configured through the `performance.sampleRate` parameter (value from 0 to 1) - Only transactions of type 'default' with finish status 'success' are subject to sampling - Sampling process: @@ -85,6 +93,7 @@ When completing a transaction: 2. If the number is less than or equal to sampleRate, the transaction is sent ## Transaction Aggregation + 1. [Transactions](#transaction) are grouped by name (name field) 2. For each group, statistical indicators are calculated: - minStartTime - earliest start time @@ -94,8 +103,7 @@ When completing a transaction: - maxDuration - maximum duration 3. Based on this data, [AggregatedTransaction](#aggregatedtransaction) objects are created - 4. For each aggregated transaction: - [Spans](#span) are grouped by name - - The same statistical indicators are calculated for each span group + - Their own statistical indicators (see [AggregatedSpan](#aggregatedspan)) are calculated for each span group - [AggregatedSpan](#aggregatedspan) objects are created From e510e2a16db0b6d8de9e8f4bf5c162ac434f66ca Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sun, 16 Mar 2025 16:43:12 +0300 Subject: [PATCH 26/37] Update performance-monitoring.md --- docs/performance-monitoring.md | 62 +++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/performance-monitoring.md b/docs/performance-monitoring.md index d89dd2a..c3eb6d9 100644 --- a/docs/performance-monitoring.md +++ b/docs/performance-monitoring.md @@ -1,5 +1,59 @@ # Performance Monitoring + +## Optimizations + +If we simply send all transactions without filtering and grouping, it will create a huge load on the service. We need to balance between data completeness and the efficiency of storage and processing. + +1. Which transactions need to be sent? + + **Problem: Too Much Data in case of full sending** + + If we send every transaction: + + - The load on the server increases sharply (especially with high traffic). + - The huge amount of the data is duplicate information, which does not significantly affect the analysis. + - "Infinite loops" in client code may generate endless transactions. + + **Solution: Smart Sampling and Grouping** + + We do not need all transactions, but we need representative data. + +2. How to reduce data flow without losing quality? + + **Optimization #1: Sampling (Sending Part of the Data Randomly)** + + See [Sampling](#sampling) + + How not to lose rare and slow requests? + - You can increase the sampling chance for slow transactions (P95 > 500ms is sent with a probability of 50-100%). + - Errors (finishStatus == 'Failure') are always sent. + + **Optimization #2: Aggregation of Identical Transactions Before Sending** + + Throttling + transaction batches → instead of 1000 separate messages, send 1 AggregatedTransaction. + + - Combine transactions with the same name (e.g., GET /api/users) and time window (e.g., 1 second). + - Instead of 1000 transactions, send one with count = 1000 and average metrics. + - How to choose the time? + - Store P50, P95, P100 (percentiles). + - Save min(startTime) and max(endTime) to see the interval boundaries and calculate Transactions Per Minute. + + What do we lose? + - The detail of each specific transaction. + - Exact startTime and endTime for each transaction. + + What do we gain? + - A sharp reduction in load on the Collector and DB (10-100 times fewer records). + - All necessary metrics (P50, P95, P100, avg) remain. + - You can continue to build graphs and calculate metrics, but with less load. + + **Optimization #3: Filtering "Garbage"** + + Transactions with duration < `thresholdMs` will not be sent, as they are not critical. + + + ## Data types ### Transaction @@ -17,7 +71,7 @@ ### AggregatedTransaction | Field | Type | Description | |-------|------|-------------| -| aggregationId | string | Identifier of the aggregation | +| aggregationId | string | Identifier of the aggregation | | name | string | Name of the transaction | | avgStartTime | number | Average timestamp when transaction started | | minStartTime | number | Minimum timestamp when transaction started | @@ -25,8 +79,8 @@ | p50duration | number | 50th percentile (median) duration of transaction in milliseconds | | p95duration | number | 95th percentile duration of transaction in milliseconds | | maxDuration | number | Maximum duration of transaction in milliseconds | -| count | number | how many transactions aggregated | -| aggregatedSpans | AggregatedSpan[] | List of spans in transactions | +| count | number | how many transactions aggregated | +| aggregatedSpans | AggregatedSpan[] | List of spans in transactions | ### Span @@ -74,7 +128,7 @@ When completing a transaction: - `finishStatus` "failure" has a priority over `thresholdMs` - Otherwise, the transaction goes through the [sampling process](#sampling) -3. After successful sampling, the transaction is added to the list for sending +3. After successful sampling, the transaction is added to the list for sending ### 3. Sending Transactions From cefde4d4db41e06983ab3c6243a6200f557d2ce5 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sun, 16 Mar 2025 16:44:30 +0300 Subject: [PATCH 27/37] Update performance-monitoring.md --- docs/performance-monitoring.md | 48 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/docs/performance-monitoring.md b/docs/performance-monitoring.md index c3eb6d9..87cb6b2 100644 --- a/docs/performance-monitoring.md +++ b/docs/performance-monitoring.md @@ -1,13 +1,12 @@ # Performance Monitoring - ## Optimizations If we simply send all transactions without filtering and grouping, it will create a huge load on the service. We need to balance between data completeness and the efficiency of storage and processing. -1. Which transactions need to be sent? +### 1. Which transactions need to be sent? - **Problem: Too Much Data in case of full sending** +#### Problem: Too Much Data in case of full sending If we send every transaction: @@ -15,43 +14,48 @@ If we simply send all transactions without filtering and grouping, it will creat - The huge amount of the data is duplicate information, which does not significantly affect the analysis. - "Infinite loops" in client code may generate endless transactions. - **Solution: Smart Sampling and Grouping** +#### Solution: Smart Sampling and Grouping We do not need all transactions, but we need representative data. -2. How to reduce data flow without losing quality? - - **Optimization #1: Sampling (Sending Part of the Data Randomly)** +### 2. How to reduce data flow without losing quality? - See [Sampling](#sampling) +#### Optimization #1: Sampling (Sending Part of the Data Randomly) How not to lose rare and slow requests? - - You can increase the sampling chance for slow transactions (P95 > 500ms is sent with a probability of 50-100%). - - Errors (finishStatus == 'Failure') are always sent. - **Optimization #2: Aggregation of Identical Transactions Before Sending** + - We can increase the sampling chance for slow transactions (P95 > 500ms is sent with a probability of 50-100%). + - Errors (`status` == 'Failure') are always sent. + + See [Sampling](#sampling) for details. + +#### Optimization #2: Aggregation of Identical Transactions Before Sending + + Throttling + transaction batches → instead of 1000 separate messages, send 1 `AggregatedTransaction`. + +##### Combine transactions with the same name (e.g., GET /api/users) and time window (e.g., 1 second). + +Instead of 1000 transactions, send one with count = 1000 and average metrics. - Throttling + transaction batches → instead of 1000 separate messages, send 1 AggregatedTransaction. +##### How to choose the time? - - Combine transactions with the same name (e.g., GET /api/users) and time window (e.g., 1 second). - - Instead of 1000 transactions, send one with count = 1000 and average metrics. - - How to choose the time? - Store P50, P95, P100 (percentiles). - Save min(startTime) and max(endTime) to see the interval boundaries and calculate Transactions Per Minute. - What do we lose? + **What do we lose ?** - The detail of each specific transaction. - Exact startTime and endTime for each transaction. - What do we gain? + **What do we gain?** - A sharp reduction in load on the Collector and DB (10-100 times fewer records). - All necessary metrics (P50, P95, P100, avg) remain. - You can continue to build graphs and calculate metrics, but with less load. - **Optimization #3: Filtering "Garbage"** + See [Transaction Aggregation](#transaction-aggregation) for details on how transactions are aggregated. - Transactions with duration < `thresholdMs` will not be sent, as they are not critical. +#### Optimization #3: Filtering "Garbage" +Transactions with duration < `thresholdMs` will not be sent, as they are not critical. ## Data types @@ -65,7 +69,7 @@ If we simply send all transactions without filtering and grouping, it will creat | startTime | number | Timestamp when transaction started | | endTime | number | Timestamp when transaction ended | | duration | number | Total duration of transaction in milliseconds | -| finishStatus | string | Status when transaction finished. 'success' (default) or 'failure'. See [Transaction Completion](#2-transaction-completion) | +| status | string | Status when transaction finished. 'success' (default) or 'failure'. See [Transaction Completion](#2-transaction-completion) | | spans | Span[] | Array of [spans](#span) associated with this transaction | ### AggregatedTransaction @@ -118,14 +122,14 @@ When creating a transaction, you can specify its type: When completing a transaction: -1. A finish status is specified (finishStatus): +1. A finish status is specified (`status`): - 'success' (default) - successful completion - 'failure' - completion with error (such transactions are always sent to the server) 2. The transaction duration is checked: - If `thresholdMs` parameter is specified and the transaction duration is less than this value, the transaction is discarded - Default `thresholdMs` is 20ms - - `finishStatus` "failure" has a priority over `thresholdMs` + - `status` "failure" has a priority over `thresholdMs` - Otherwise, the transaction goes through the [sampling process](#sampling) 3. After successful sampling, the transaction is added to the list for sending From e6885ca272d001ae4359133587d60c7f9154d736 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sun, 16 Mar 2025 19:31:03 +0300 Subject: [PATCH 28/37] Update performance-monitoring.md --- docs/performance-monitoring.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/performance-monitoring.md b/docs/performance-monitoring.md index 87cb6b2..9b4a708 100644 --- a/docs/performance-monitoring.md +++ b/docs/performance-monitoring.md @@ -84,6 +84,7 @@ Transactions with duration < `thresholdMs` will not be sent, as they are not cri | p95duration | number | 95th percentile duration of transaction in milliseconds | | maxDuration | number | Maximum duration of transaction in milliseconds | | count | number | how many transactions aggregated | +| failureRate | number | percentage of transactions with status 'failure' | | aggregatedSpans | AggregatedSpan[] | List of spans in transactions | @@ -95,6 +96,7 @@ Transactions with duration < `thresholdMs` will not be sent, as they are not cri | startTime | number | Timestamp when span started | | endTime | number | Timestamp when span ended | | duration | number | Total duration of span in milliseconds | +| status | string | Status when span finished. 'success' (default) or 'failure' | ### AggregatedSpan See [Transaction Aggregation](#transaction-aggregation) for details on how spans are aggregated. @@ -108,6 +110,7 @@ See [Transaction Aggregation](#transaction-aggregation) for details on how spans | p50duration | number | 50th percentile (median) duration of span in milliseconds | | p95duration | number | 95th percentile duration of span in milliseconds | | maxDuration | number | Maximum duration of span in milliseconds | +| failureRate | number | percentage of spans with status 'failure' | ## Transaction Lifecycle From 366ca89dc6f9d2abc566a668c1b5f22a23c69828 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 20:40:40 +0300 Subject: [PATCH 29/37] Update respectively to docs --- example/monitoring.html | 228 +++++++++++++++++++++++ src/catcher.ts | 19 +- src/modules/performance/index.ts | 247 +++++++++++++++---------- src/modules/performance/span.ts | 17 +- src/modules/performance/transaction.ts | 119 ++++++------ src/modules/performance/types.ts | 46 +++++ src/types/hawk-initial-settings.ts | 39 ++-- src/types/performance-message.ts | 4 +- src/types/transaction.ts | 24 +++ 9 files changed, 565 insertions(+), 178 deletions(-) create mode 100644 example/monitoring.html create mode 100644 src/modules/performance/types.ts create mode 100644 src/types/transaction.ts diff --git a/example/monitoring.html b/example/monitoring.html new file mode 100644 index 0000000..f1f2a49 --- /dev/null +++ b/example/monitoring.html @@ -0,0 +1,228 @@ + + + + + + Hawk.js Demo + + + +

Hawk.js Demo

+ +

Performance Monitoring Settings Demo

+
+

Configuration

+ + + + + + + + + +

Generate Transactions

+
+ + + + + +
+ +

Aggregation Testing

+
+ + + +
+
+ +

Results

+
+ + + + diff --git a/src/catcher.ts b/src/catcher.ts index c4eb1f0..0169348 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -16,8 +16,8 @@ import type { JavaScriptCatcherIntegrations } from './types/integrations'; import { EventRejectedError } from './errors'; import type { HawkJavaScriptEvent } from './types'; import { isErrorProcessed, markErrorAsProcessed } from './utils/event'; -import PerformanceMonitoring from './modules/performance'; -import { Transaction } from './modules/performance/transaction'; +import type { Transaction } from './modules/performance/transaction'; +import PerformanceMonitoring from './modules/performance/index'; /** * Allow to use global VERSION, that will be overwritten by Webpack @@ -158,19 +158,23 @@ export default class Catcher { if (settings.performance) { const sampleRate = typeof settings.performance === 'object' && typeof settings.performance.sampleRate === 'number' ? settings.performance.sampleRate : - 1.0; + undefined; const batchInterval = typeof settings.performance === 'object' && typeof settings.performance.batchInterval === 'number' ? settings.performance.batchInterval : undefined; + const thresholdMs = typeof settings.performance === 'object' && typeof settings.performance.thresholdMs === 'number' ? + settings.performance.thresholdMs : + undefined; + this.performance = new PerformanceMonitoring( this.transport, this.token, - this.version, this.debug, sampleRate, - batchInterval + batchInterval, + thresholdMs ); } } @@ -249,14 +253,13 @@ export default class Catcher { * Starts a new transaction * * @param name - Name of the transaction (e.g., 'page-load', 'api-request') - * @param tags - Key-value pairs for additional transaction data */ - public startTransaction(name: string, tags: Record = {}): Transaction | undefined { + public startTransaction(name: string): Transaction | undefined { if (this.performance === null) { console.error('Hawk: can not start transaction. Performance monitoring is not enabled. Please enable it by setting performance: true in the HawkCatcher constructor.'); } - return this.performance?.startTransaction(name, tags); + return this.performance?.startTransaction(name); } /** diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index 69daab0..17c3712 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -1,17 +1,27 @@ import type { PerformanceMessage } from '../../types/performance-message'; -import { id } from '../../utils/id'; import log from '../../utils/log'; import type Socket from '../socket'; -import { isBrowser } from '../../utils/is-browser'; -import { getTimestamp } from '../../utils/get-timestamp'; -import { Transaction, SampledOutTransaction } from './transaction'; +import { Transaction } from './transaction'; +import type { AggregatedTransaction, AggregatedSpan } from '../../types/transaction'; +import type { Span } from './span'; /** * Default interval between batch sends in milliseconds */ const DEFAULT_BATCH_INTERVAL = 3000; +/** + * Default sample rate for performance monitoring + * Value of 1.0 means all transactions will be sampled + */ +const DEFAULT_SAMPLE_RATE = 1.0; + +/** + * Default threshold in milliseconds for filtering out short transactions + * Transactions shorter than this duration will not be sent + */ +const DEFAULT_THRESHOLD_MS = 20; /** * Class for managing performance monitoring @@ -22,56 +32,38 @@ export default class PerformanceMonitoring { */ private batchTimeout: ReturnType | null = null; - /** - * Map of active transactions by their ID - * Used to: - * - Track transactions that haven't been finished yet - * - Finish all active transactions on page unload/process exit - * - Prevent memory leaks by removing finished transactions - */ - private activeTransactions: Map = new Map(); - /** * Queue for transactions waiting to be sent */ private sendQueue: Transaction[] = []; /** - * Sample rate for performance data - * Used to determine if a transaction should be sampled out + * Sample rate for performance monitoring */ private readonly sampleRate: number; /** * @param transport - Transport instance for sending data * @param token - Integration token - * @param version - Catcher version * @param debug - Debug mode flag - * @param sampleRate - Sample rate for performance data (0.0 to 1.0) - * @param batchInterval - Interval between batch sends in milliseconds + * @param sampleRate - Sample rate for performance data (0.0 to 1.0). Must be between 0 and 1. + * @param batchInterval - Interval between batch sends in milliseconds. Defaults to 3000ms. + * @param thresholdMs - Minimum duration threshold in milliseconds. Transactions shorter than this will be filtered out. Defaults to 1000ms. */ constructor( private readonly transport: Socket, private readonly token: string, - private readonly version: string, private readonly debug: boolean = false, - sampleRate: number = 1.0, - private readonly batchInterval: number = DEFAULT_BATCH_INTERVAL + sampleRate: number = DEFAULT_SAMPLE_RATE, + private readonly batchInterval: number = DEFAULT_BATCH_INTERVAL, + private readonly thresholdMs: number = DEFAULT_THRESHOLD_MS ) { if (sampleRate < 0 || sampleRate > 1) { console.error('Performance monitoring sample rate must be between 0 and 1'); sampleRate = 1; } - this.sampleRate = Math.max(0, Math.min(1, sampleRate)); - - if (isBrowser) { - this.initBeforeUnloadHandler(); - } else { - this.initProcessExitHandler(); - } - // Start batch sending timer - this.scheduleBatchSend(); + this.sampleRate = Math.max(0, Math.min(1, sampleRate)); } /** @@ -80,8 +72,11 @@ export default class PerformanceMonitoring { * @param transaction */ public queueTransaction(transaction: Transaction): void { - this.activeTransactions.delete(transaction.id); this.sendQueue.push(transaction); + + if (this.sendQueue.length === 1) { + this.scheduleBatchSend(); + } } /** @@ -89,32 +84,19 @@ export default class PerformanceMonitoring { * * @param name - Transaction name * @param tags - Optional tags for the transaction + * @param severity * @returns Transaction object */ - public startTransaction(name: string, tags: Record = {}): Transaction { + public startTransaction(name: string, severity: 'default' | 'critical' = 'default'): Transaction { const data = { - id: id(), name, - startTime: getTimestamp(), - tags, - spans: [], + severity, }; - - // Sample transactions based on rate - if (Math.random() > this.sampleRate) { - if (this.debug) { - log(`Transaction "${name}" was sampled out`, 'info'); - } - - return new SampledOutTransaction(data); - } - - const transaction = new Transaction(data, this); - - this.activeTransactions.set(transaction.id, transaction); - - return transaction; + return new Transaction(data, this, { + sampleRate: this.sampleRate, + thresholdMs: this.thresholdMs, + }); } /** @@ -123,57 +105,22 @@ export default class PerformanceMonitoring { public destroy(): void { // Clear batch sending timer if (this.batchTimeout !== null) { - const clear = isBrowser ? window.clearTimeout : clearTimeout; - - clear(this.batchTimeout); + clearTimeout(this.batchTimeout); this.batchTimeout = null; } - // Finish any remaining transactions - this.activeTransactions.forEach(transaction => transaction.finish()); - // Force send any remaining queued data if (this.sendQueue.length > 0) { void this.processSendQueue(); } } - /** - * Initialize handler for browser page unload - */ - private initBeforeUnloadHandler(): void { - window.addEventListener('beforeunload', () => { - // Finish any active transactions - this.activeTransactions.forEach(transaction => transaction.finish()); - }); - } - - /** - * Initialize handler for Node.js process exit - */ - private initProcessExitHandler(): void { - process.on('beforeExit', () => { - // Finish any active transactions before exit - this.activeTransactions.forEach(transaction => transaction.finish()); - }); - - ['SIGINT', 'SIGTERM'].forEach(signal => { - process.on(signal, () => { - // Prevent immediate exit - this.destroy(); - process.exit(0); - }); - }); - } - /** * Schedule periodic batch sending of transactions */ private scheduleBatchSend(): void { this.batchTimeout = setTimeout(() => { void this.processSendQueue(); - - this.scheduleBatchSend(); // Schedule next batch }, this.batchInterval); } @@ -185,15 +132,16 @@ export default class PerformanceMonitoring { return; } - // Get all transactions from queue const transactions = [ ...this.sendQueue ]; this.sendQueue = []; try { - await this.sendPerformanceData(transactions); + const aggregatedTransactions = this.aggregateTransactions(transactions); + + await this.sendPerformanceData(aggregatedTransactions); } catch (error) { - // Return failed transactions to queue + // todo: add repeats limit this.sendQueue.push(...transactions); if (this.debug) { @@ -202,20 +150,133 @@ export default class PerformanceMonitoring { } } + /** + * Aggregates transactions into statistical summaries grouped by name + * + * @param transactions + */ + private aggregateTransactions(transactions: Transaction[]): AggregatedTransaction[] { + const transactionsByName = new Map(); + + // Group transactions by name + transactions.forEach(transaction => { + const group = transactionsByName.get(transaction.name) || []; + + group.push(transaction); + transactionsByName.set(transaction.name, group); + }); + + // Aggregate each group + return Array.from(transactionsByName.entries()).map(([name, group]) => { + const durations = group.map(t => t.duration ?? 0).sort((a, b) => a - b); + const startTimes = group.map(t => t.startTime ?? 0); + const endTimes = group.map((transaction, index) => transaction.endTime ?? startTimes[index]); + + // Calculate failure rate + const failureCount = group.filter(t => t.finishStatus === 'failure').length; + const failureRate = (failureCount / group.length) * 100; + + return { + aggregationId: `${name}-${Date.now()}`, + name, + avgStartTime: this.average(startTimes), + minStartTime: Math.min(...startTimes), + maxEndTime: Math.max(...endTimes), + p50duration: this.percentile(durations, 50), + p95duration: this.percentile(durations, 95), + maxDuration: Math.max(...durations), + count: group.length, + failureRate, + aggregatedSpans: this.aggregateSpans(group), + }; + }); + } + + /** + * + * @param transactions + */ + private aggregateSpans(transactions: Transaction[]): AggregatedSpan[] { + const spansByName = new Map(); + + transactions.forEach(transaction => { + transaction.spans.forEach(span => { + const spans = spansByName.get(span.name) || []; + + spans.push(span); + spansByName.set(span.name, spans); + }); + }); + + return Array.from(spansByName.entries()).map(([name, spans]) => { + const durations = spans.map(s => s.duration ?? 0).sort((a, b) => a - b); + const startTimes = spans.map(s => s.startTime ?? 0); + const endTimes = spans.map(s => s.endTime ?? 0); + + return { + aggregationId: `${name}-${Date.now()}`, + name, + minStartTime: Math.min(...startTimes), + maxEndTime: Math.max(...endTimes), + p50duration: this.percentile(durations, 50), + p95duration: this.percentile(durations, 95), + maxDuration: Math.max(...durations), + }; + }); + } + + /** + * + * @param sortedValues + * @param p + */ + private percentile(sortedValues: number[], p: number): number { + const index = Math.ceil((p / 100) * sortedValues.length) - 1; + + return sortedValues[index]; + } + + /** + * + * @param values + */ + private average(values: number[]): number { + return values.reduce((a, b) => a + b, 0) / values.length; + } + /** * Sends performance data to Hawk collector * - * @param transactions - Array of transactions to send + * @param transactions - Array of aggregated transactions to send */ - private async sendPerformanceData(transactions: Transaction[]): Promise { + private async sendPerformanceData(transactions: AggregatedTransaction[]): Promise { const performanceMessage: PerformanceMessage = { token: this.token, catcherType: 'performance', payload: { - transactions: transactions.map(transaction => transaction.getData()) + transactions, }, }; await this.transport.send(performanceMessage); } } + +/** + * + */ +export class Performance { + private static instance: Performance; + + /** + * + */ + static getInstance(): Performance { + if (!Performance.instance) { + Performance.instance = new Performance(); + } + + return Performance.instance; + } + // ... rest of the class +} diff --git a/src/modules/performance/span.ts b/src/modules/performance/span.ts index c4aa04d..dbd1248 100644 --- a/src/modules/performance/span.ts +++ b/src/modules/performance/span.ts @@ -1,13 +1,19 @@ -import { getTimestamp } from "../../utils/get-timestamp"; +import { id } from '../../utils/id'; +import { getTimestamp } from '../../utils/get-timestamp'; + +interface SpanConstructionData { + transactionId: string; + name: string; +} /** * Class representing a span of work within a transaction */ export class Span { - public readonly id: string; + public readonly id: string = id(); public readonly transactionId: string; public readonly name: string; - public readonly startTime: number; + public readonly startTime: number = getTimestamp(); public endTime?: number; public duration?: number; public readonly metadata?: Record; @@ -17,8 +23,9 @@ export class Span { * * @param data - Data to initialize the span with. Contains id, transactionId, name, startTime, metadata */ - constructor(data: Omit) { - Object.assign(this, data); + constructor(data: SpanConstructionData) { + this.transactionId = data.transactionId; + this.name = data.name; } /** diff --git a/src/modules/performance/transaction.ts b/src/modules/performance/transaction.ts index 1446ed7..dcddc0b 100644 --- a/src/modules/performance/transaction.ts +++ b/src/modules/performance/transaction.ts @@ -1,63 +1,66 @@ -import { getTimestamp } from "../../utils/get-timestamp"; -import PerformanceMonitoring from "."; -import { Span } from "./span"; -import { id } from "../../utils/id"; +import { getTimestamp } from '../../utils/get-timestamp'; +import type PerformanceMonitoring from '.'; +import { Span } from './span'; +import { id } from '../../utils/id'; +import type { PerformanceMonitoringConfig } from './types'; -export interface TransactionData { - id: string; +/** + * Interface representing data needed to construct a Transaction + */ +export interface TransactionConstructionData { + /** + * Name of the transaction (e.g. 'page-load', 'api-call') + */ name: string; - startTime: number; - endTime?: number; - duration?: number; - tags: Record; - spans: Span[]; + + /** + * Severity level of the transaction + * - 'default': Normal transaction that might be sampled out + * - 'critical': High priority transaction that will be sent regardless of sampling rate + */ + severity: 'default' | 'critical'; } /** * Class representing a transaction that can contain multiple spans */ export class Transaction { - public readonly id: string; + public readonly id: string = id(); public readonly name: string; - public readonly startTime: number; + public readonly startTime: number = getTimestamp(); public endTime?: number; public duration?: number; - public readonly tags: Record; public readonly spans: Span[] = []; + public finishStatus: 'success' | 'failure' = 'success'; + private severity: 'default' | 'critical' = 'default'; /** * Constructor for Transaction * * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags * @param performance - Reference to the PerformanceMonitoring instance that created this transaction + * @param config - Configuration for this transaction */ constructor( - data: TransactionData, - private readonly performance: PerformanceMonitoring + data: TransactionConstructionData, + private readonly performance: PerformanceMonitoring, + private readonly config: PerformanceMonitoringConfig ) { - this.id = data.id; this.name = data.name; - this.startTime = data.startTime; - this.endTime = data.endTime; - this.duration = data.duration; - this.tags = data.tags; - this.spans = data.spans; + this.severity = data.severity; } /** * Starts a new span within this transaction - * + * * @param name - Name of the span * @param metadata - Optional metadata to attach to the span * @returns New span instance */ - public startSpan(name: string, metadata?: Record): Span { + public startSpan(name: string): Span { const data = { - id: id(), transactionId: this.id, name, - startTime: getTimestamp(), - metadata, }; const span = new Span(data); @@ -68,49 +71,55 @@ export class Transaction { } /** + * Finishes the transaction and queues it for sending if: + * - It's a failure or has critical severity + * - Its duration is above the threshold and passes sampling * + * @param status - Status of the transaction ('success' or 'failure'). Defaults to 'success' */ - public finish(): void { - // Finish all unfinished spans - this.spans.forEach(span => { - if (!span.endTime) { - span.finish(); - } - }); - + public finish(status: 'success' | 'failure' = 'success'): void { this.endTime = getTimestamp(); this.duration = this.endTime - this.startTime; - this.performance.queueTransaction(this); - } + this.finishStatus = status; - /** - * Returns transaction data that should be sent to server - */ - public getData(): TransactionData { - const { performance, ...data } = this; + // Always send if it's a failure or critical severity + if (status === 'failure' || this.severity === 'critical') { + this.queueForSending(); + + return; + } + + // Filter out short transactions + if (this.duration < this.config.thresholdMs) { + return; + } - return data; + // Apply sampling + if (this.shouldSample()) { + this.queueForSending(); + } } -} -/** - * Class representing a sampled out transaction that won't be sent to server - */ -export class SampledOutTransaction extends Transaction { /** - * Constructor for SampledOutTransaction + * Determines if this transaction should be sampled based on configured sample rate * - * @param data - Data to initialize the transaction with. Contains id, name, startTime, tags and spans + * @returns True if transaction should be sampled, false otherwise */ - constructor(data: TransactionData) { - super(data, null as unknown as PerformanceMonitoring); + private shouldSample(): boolean { + return Math.random() <= this.config.sampleRate; } + /** + * Queues this transaction for sending to the server + */ + private queueForSending(): void { + this.performance.queueTransaction(this); + } /** - * Finishes the transaction but does not send it to server since it was sampled out + * */ - public finish(): void { - // Do nothing - don't send to server + public get status(): 'success' | 'failure' { + return this.finishStatus; } } diff --git a/src/modules/performance/types.ts b/src/modules/performance/types.ts new file mode 100644 index 0000000..da660ed --- /dev/null +++ b/src/modules/performance/types.ts @@ -0,0 +1,46 @@ +export interface Transaction { + id: string; + severity: 'default' | 'critical'; + name: string; + startTime: number; + endTime: number; + duration: number; + status: 'success' | 'failure'; + spans: Span[]; +} + +export interface AggregatedTransaction { + aggregationId: string; + name: string; + avgStartTime: number; + minStartTime: number; + maxEndTime: number; + p50duration: number; + p95duration: number; + maxDuration: number; + count: number; + aggregatedSpans: AggregatedSpan[]; +} + +export interface Span { + id: string; + name: string; + startTime: number; + endTime: number; + duration: number; +} + +export interface AggregatedSpan { + aggregationId: string; + name: string; + minStartTime: number; + maxEndTime: number; + p50duration: number; + p95duration: number; + maxDuration: number; +} + +export interface PerformanceMonitoringConfig { + sampleRate: number; + thresholdMs: number; +} diff --git a/src/types/hawk-initial-settings.ts b/src/types/hawk-initial-settings.ts index 8c66031..27cdd86 100644 --- a/src/types/hawk-initial-settings.ts +++ b/src/types/hawk-initial-settings.ts @@ -80,19 +80,28 @@ export interface HawkInitialSettings { * - {sampleRate: number} to enable with custom sample rate * - false or undefined to disable */ - performance?: boolean | { - /** - * Sample rate for performance data (0.0 to 1.0) - * - * @default 1.0 - */ - sampleRate: number; - - /** - * Interval between batch sends in milliseconds - * - * @default 3000 - */ - batchInterval?: number; - }; + performance?: boolean | PerformanceMonitoringConfig; +} + +export interface PerformanceMonitoringConfig { + /** + * Sample rate for performance data (0.0 to 1.0) + * + * @default 1.0 + */ + sampleRate: number; + + /** + * Interval between batch sends in milliseconds + * + * @default 3000 + */ + batchInterval: number; + + /** + * Interval between batch sends in milliseconds + * + * @default 3000 + */ + thresholdMs: number; } diff --git a/src/types/performance-message.ts b/src/types/performance-message.ts index 487ef6c..d9c5aed 100644 --- a/src/types/performance-message.ts +++ b/src/types/performance-message.ts @@ -1,5 +1,5 @@ import type { EncodedIntegrationToken } from '@hawk.so/types'; -import { TransactionData } from 'src/modules/performance/transaction'; +import type { AggregatedTransaction } from './transaction'; /** * Interface for performance monitoring message @@ -19,6 +19,6 @@ export interface PerformanceMessage { * Performance monitoring data */ payload: { - transactions: TransactionData[]; + transactions: AggregatedTransaction[]; }; } diff --git a/src/types/transaction.ts b/src/types/transaction.ts new file mode 100644 index 0000000..009ca82 --- /dev/null +++ b/src/types/transaction.ts @@ -0,0 +1,24 @@ +export interface AggregatedTransaction { + aggregationId: string; + name: string; + avgStartTime: number; + minStartTime: number; + maxEndTime: number; + p50duration: number; + p95duration: number; + maxDuration: number; + count: number; + failureRate: number; + aggregatedSpans: AggregatedSpan[]; +} + +export interface AggregatedSpan { + aggregationId: string; + name: string; + minStartTime: number; + maxEndTime: number; + p50duration: number; + p95duration: number; + maxDuration: number; +} + From 936da97e52a28bc941b3cd26b5f5c194f479f952 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 21:03:45 +0300 Subject: [PATCH 30/37] Upd --- src/modules/performance/index.ts | 53 ++++++++++++++------------------ src/modules/performance/span.ts | 8 +++-- src/types/transaction.ts | 1 + 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index 17c3712..e9a9ea4 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -69,7 +69,7 @@ export default class PerformanceMonitoring { /** * Queue transaction for sending * - * @param transaction + * @param transaction - Transaction to queue */ public queueTransaction(transaction: Transaction): void { this.sendQueue.push(transaction); @@ -84,7 +84,7 @@ export default class PerformanceMonitoring { * * @param name - Transaction name * @param tags - Optional tags for the transaction - * @param severity + * @param severity - Severity of the transaction * @returns Transaction object */ public startTransaction(name: string, severity: 'default' | 'critical' = 'default'): Transaction { @@ -193,12 +193,16 @@ export default class PerformanceMonitoring { } /** - * - * @param transactions + * Aggregates spans from multiple transactions into statistical summaries + * Groups spans by name across all transactions in the group + * + * @param transactions - Transactions containing spans to aggregate + * @returns Array of aggregated spans with statistical metrics */ private aggregateSpans(transactions: Transaction[]): AggregatedSpan[] { const spansByName = new Map(); - + + // Group spans by name across all transactions transactions.forEach(transaction => { transaction.spans.forEach(span => { const spans = spansByName.get(span.name) || []; @@ -208,10 +212,15 @@ export default class PerformanceMonitoring { }); }); + // Aggregate each group of spans return Array.from(spansByName.entries()).map(([name, spans]) => { const durations = spans.map(s => s.duration ?? 0).sort((a, b) => a - b); const startTimes = spans.map(s => s.startTime ?? 0); - const endTimes = spans.map(s => s.endTime ?? 0); + const endTimes = spans.map((s, index) => s.endTime ?? startTimes[index]); + + // Calculate failure rate for spans + const failureCount = spans.filter(s => s.status === 'failure').length; + const failureRate = (failureCount / spans.length) * 100; return { aggregationId: `${name}-${Date.now()}`, @@ -221,14 +230,16 @@ export default class PerformanceMonitoring { p50duration: this.percentile(durations, 50), p95duration: this.percentile(durations, 95), maxDuration: Math.max(...durations), + failureRate }; }); } /** - * - * @param sortedValues - * @param p + * Calculates the percentile value from a sorted array of numbers + * @param sortedValues - Sorted array of numbers + * @param p - Percentile to calculate (e.g., 50 for median, 95 for 95th percentile) + * @returns Percentile value */ private percentile(sortedValues: number[], p: number): number { const index = Math.ceil((p / 100) * sortedValues.length) - 1; @@ -237,8 +248,9 @@ export default class PerformanceMonitoring { } /** - * - * @param values + * Calculates the average value from an array of numbers + * @param values - Array of numbers + * @returns Average value */ private average(values: number[]): number { return values.reduce((a, b) => a + b, 0) / values.length; @@ -261,22 +273,3 @@ export default class PerformanceMonitoring { await this.transport.send(performanceMessage); } } - -/** - * - */ -export class Performance { - private static instance: Performance; - - /** - * - */ - static getInstance(): Performance { - if (!Performance.instance) { - Performance.instance = new Performance(); - } - - return Performance.instance; - } - // ... rest of the class -} diff --git a/src/modules/performance/span.ts b/src/modules/performance/span.ts index dbd1248..4a35af5 100644 --- a/src/modules/performance/span.ts +++ b/src/modules/performance/span.ts @@ -17,6 +17,7 @@ export class Span { public endTime?: number; public duration?: number; public readonly metadata?: Record; + public status: 'success' | 'failure' = 'success'; /** * Constructor for Span @@ -29,10 +30,13 @@ export class Span { } /** - * Finishes the span by setting the end time and calculating duration + * Finishes the span and calculates its duration + * + * @param status - Status of the span ('success' or 'failure'). Defaults to 'success' */ - public finish(): void { + public finish(status: 'success' | 'failure' = 'success'): void { this.endTime = getTimestamp(); this.duration = this.endTime - this.startTime; + this.status = status; } } diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 009ca82..ae783a0 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -20,5 +20,6 @@ export interface AggregatedSpan { p50duration: number; p95duration: number; maxDuration: number; + failureRate: number; } From 6ffcf162697a6186f8ac897d65ab77a1d592dad5 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 21:07:31 +0300 Subject: [PATCH 31/37] Update readme --- README.md | 137 +++++++++++++++++++++++++----------------------------- 1 file changed, 63 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 7795c72..47f3d21 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Initialization settings: | `disableGlobalErrorsHandling` | boolean | optional | Do not initialize global errors handling | | `disableVueErrorHandler` | boolean | optional | Do not initialize Vue errors handling | | `beforeSend` | function(event) => event | optional | This Method allows you to filter any data you don't want sending to Hawk | -| `performance` | boolean\|object | optional | Performance monitoring settings. When object, accepts:
- `sampleRate`: Sample rate (0.0 to 1.0, default: 1.0)
- `batchInterval`: Batch send interval in ms (default: 3000) | +| `performance` | boolean\|object | optional | Performance monitoring settings. When object, accepts:
- `sampleRate`: Sample rate (0.0 to 1.0, default: 1.0)
- `thresholdMs`: Minimum duration threshold in ms (default: 20)
- `batchInterval`: Batch send interval in ms (default: 3000) | Other available [initial settings](types/hawk-initial-settings.d.ts) are described at the type definition. @@ -166,9 +166,15 @@ hawk.connectVue(Vue) The SDK can monitor performance of your application by tracking transactions and spans. -### Transaction Batching +### Transaction Batching and Aggregation + +Transactions are collected, aggregated, and sent in batches to reduce network overhead and provide statistical insights: + +- Transactions with the same name are grouped together +- Statistical metrics are calculated (p50, p95, max durations) +- Spans are aggregated across transactions +- Failure rates are tracked for both transactions and spans -By default, transactions are collected and sent in batches every 3 seconds to reduce network overhead. You can configure the batch interval using the `batchInterval` option: ```js @@ -180,12 +186,7 @@ const hawk = new HawkCatcher({ }); ``` -Transactions are automatically batched and sent: -- Every `batchInterval` milliseconds -- When the page is unloaded (in browser) -- When the process exits (in Node.js) - -### Sampling +### Sampling and Filtering You can configure what percentage of transactions should be sent to Hawk using the `sampleRate` option: @@ -193,78 +194,45 @@ You can configure what percentage of transactions should be sent to Hawk using t const hawk = new HawkCatcher({ token: 'INTEGRATION_TOKEN', performance: { - sampleRate: 0.2 + sampleRate: 0.2, // Sample 20% of transactions + thresholdMs: 50 // Only send transactions longer than 50ms } }); ``` -Features: -- Track transactions and spans with timing data -- Automatic span completion when transaction ends -- Support for both browser and Node.js environments -- Debug mode for development -- Throttled data sending to prevent server overload -- Graceful cleanup on page unload/process exit - -> Note: If performance monitoring is not enabled, `startTransaction()` will return undefined and log an error to the console. +Transactions are automatically filtered based on: +- Duration threshold (transactions shorter than `thresholdMs` are ignored) +- Sample rate (random sampling based on `sampleRate`) +- Severity (critical transactions are always sent regardless of sampling) +- Status (failed transactions are always sent regardless of sampling) ### API Reference -#### startTransaction(name: string, tags?: Record): Transaction +#### startTransaction(name: string, severity?: 'default' | 'critical'): Transaction Starts a new transaction. A transaction represents a high-level operation like a page load or an API call. - `name`: Name of the transaction -- `tags`: Optional key-value pairs for additional transaction data - -#### startSpan(transactionId: string, name: string, metadata?: Record): Span - -Creates a new span within a transaction. Spans represent smaller units of work within a transaction. - -- `transactionId`: ID of the parent transaction -- `name`: Name of the span -- `metadata`: Optional metadata for the span - -#### finishSpan(spanId: string): void +- `severity`: Optional severity level. 'critical' transactions are always sent regardless of sampling. -Finishes a span and calculates its duration. +#### Transaction Methods -- `spanId`: ID of the span to finish - -#### finishTransaction(transactionId: string): void - -Finishes a transaction, calculates its duration, and sends the performance data to Hawk. - -- `transactionId`: ID of the transaction to finish - -### Data Model - -#### Transaction ```typescript interface Transaction { - id: string; - name: string; - startTime: number; - endTime?: number; - duration?: number; - tags: Record; - spans: Span[]; - startSpan(name: string, metadata?: Record): Span; - finish(): void; + // Start a new span within this transaction + startSpan(name: string): Span; + + // Finish the transaction with optional status + finish(status?: 'success' | 'failure'): void; } ``` -#### Span +#### Span Methods + ```typescript interface Span { - id: string; - transactionId: string; - name: string; - startTime: number; - endTime?: number; - duration?: number; - metadata?: Record; - finish(): void; + // Finish the span with optional status + finish(status?: 'success' | 'failure'): void; } ``` @@ -282,10 +250,7 @@ const hawk = new HawkCatcher({ }); router.beforeEach((to, from, next) => { - const transaction = hawk.startTransaction('route-change', { - from: from.path, - to: to.path - }); + const transaction = hawk.startTransaction('route-change'); next(); @@ -296,29 +261,53 @@ router.beforeEach((to, from, next) => { }); ``` -#### Measuring API Calls +#### Measuring API Calls with Error Handling ```javascript async function fetchUsers() { const transaction = hawk.startTransaction('fetch-users'); - const apiSpan = transaction.startSpan('GET /api/user', { - url: '/api/users', - method: 'GET' - }); + const apiSpan = transaction.startSpan('api-call'); try { const response = await fetch('/api/users'); - const data = await response.json(); - apiSpan.finish(); + if (!response.ok) { + apiSpan.finish('failure'); + transaction.finish('failure'); + return null; + } + + const data = await response.json(); + apiSpan.finish('success'); const processSpan = transaction.startSpan('process-data'); // Process data... processSpan.finish(); + transaction.finish('success'); return data; - } finally { - transaction.finish(); + } catch (error) { + apiSpan.finish('failure'); + transaction.finish('failure'); + throw error; + } +} +``` + +#### Critical Transactions +```javascript +function processPayment(paymentDetails) { + // Mark as critical to ensure it's always sent regardless of sampling + const transaction = hawk.startTransaction('payment-processing', 'critical'); + + try { + // Payment processing logic... + + transaction.finish('success'); + return true; + } catch (error) { + transaction.finish('failure'); + throw error; } } ``` From e00786833bcd391395306d0bc17b2999086acf2e Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 22:07:53 +0300 Subject: [PATCH 32/37] Refactor HawkCatcher initialization in monitoring.html to use a constant for the token and remove unused transaction generation buttons for improved clarity and performance. --- example/monitoring.html | 48 +++++++++-------------------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/example/monitoring.html b/example/monitoring.html index f1f2a49..faac9da 100644 --- a/example/monitoring.html +++ b/example/monitoring.html @@ -79,8 +79,6 @@

Generate Transactions

Aggregation Testing

- -
@@ -90,10 +88,12 @@

Results

From f11bdd173c13a8503eceb53138ccd1bb47aedc1b Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 22:53:33 +0300 Subject: [PATCH 33/37] Enhance performance monitoring by adding critical duration threshold for transactions. Update documentation and README to reflect new settings, including changes to transaction filtering criteria. Remove unused demo elements from example HTML for improved clarity. --- README.md | 3 +- docs/performance-monitoring.md | 23 ++- example/index.html | 223 +------------------------ src/modules/performance/index.ts | 15 +- src/modules/performance/transaction.ts | 6 + src/modules/performance/types.ts | 1 + src/types/hawk-initial-settings.ts | 13 +- 7 files changed, 43 insertions(+), 241 deletions(-) diff --git a/README.md b/README.md index 47f3d21..23c9719 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Initialization settings: | `disableGlobalErrorsHandling` | boolean | optional | Do not initialize global errors handling | | `disableVueErrorHandler` | boolean | optional | Do not initialize Vue errors handling | | `beforeSend` | function(event) => event | optional | This Method allows you to filter any data you don't want sending to Hawk | -| `performance` | boolean\|object | optional | Performance monitoring settings. When object, accepts:
- `sampleRate`: Sample rate (0.0 to 1.0, default: 1.0)
- `thresholdMs`: Minimum duration threshold in ms (default: 20)
- `batchInterval`: Batch send interval in ms (default: 3000) | +| `performance` | boolean\|object | optional | Performance monitoring settings. When object, accepts:
- `sampleRate`: Sample rate (0.0 to 1.0, default: 1.0)
- `thresholdMs`: Minimum duration threshold in ms (default: 20)
- `criticalDurationThresholdMs`: Duration threshold for critical transactions in ms (default: 500)
- `batchInterval`: Batch send interval in ms (default: 3000) | Other available [initial settings](types/hawk-initial-settings.d.ts) are described at the type definition. @@ -202,6 +202,7 @@ const hawk = new HawkCatcher({ Transactions are automatically filtered based on: - Duration threshold (transactions shorter than `thresholdMs` are ignored) +- Critical duration threshold (transactions longer than `criticalDurationThresholdMs` are always sent) - Sample rate (random sampling based on `sampleRate`) - Severity (critical transactions are always sent regardless of sampling) - Status (failed transactions are always sent regardless of sampling) diff --git a/docs/performance-monitoring.md b/docs/performance-monitoring.md index 9b4a708..ea65b4a 100644 --- a/docs/performance-monitoring.md +++ b/docs/performance-monitoring.md @@ -2,30 +2,29 @@ ## Optimizations -If we simply send all transactions without filtering and grouping, it will create a huge load on the service. We need to balance between data completeness and the efficiency of storage and processing. +Sending all transactions without filtering and aggregation would create an heavy load on the service. We must carefully balance data completeness with efficient storage and processing to ensure optimal performance monitoring. -### 1. Which transactions need to be sent? +### 1. Transaction Selection Criteria #### Problem: Too Much Data in case of full sending - If we send every transaction: + Sending every transaction without filtering creates several critical issues: - - The load on the server increases sharply (especially with high traffic). - - The huge amount of the data is duplicate information, which does not significantly affect the analysis. + - Significantly increased server load, particularly during high traffic periods + - Large amounts of redundant data that provide minimal analytical value - "Infinite loops" in client code may generate endless transactions. #### Solution: Smart Sampling and Grouping - We do not need all transactions, but we need representative data. - -### 2. How to reduce data flow without losing quality? + Instead of collecting every transaction, we focus on gathering a representative sample that provides meaningful insights while minimizing data volume. +### 2. Data Flow Optimization Strategies #### Optimization #1: Sampling (Sending Part of the Data Randomly) - How not to lose rare and slow requests? + To ensure we capture important but infrequent transactions: - - We can increase the sampling chance for slow transactions (P95 > 500ms is sent with a probability of 50-100%). - - Errors (`status` == 'Failure') are always sent. + – Slow transactions are always sent. Transaction is considered slow if its duration is greater than criticalDurationThresholdMs parameter. + - Errors (`status` == 'failure') are always sent. See [Sampling](#sampling) for details. @@ -33,7 +32,7 @@ If we simply send all transactions without filtering and grouping, it will creat Throttling + transaction batches → instead of 1000 separate messages, send 1 `AggregatedTransaction`. -##### Combine transactions with the same name (e.g., GET /api/users) and time window (e.g., 1 second). +##### Combine transactions with the same name (e.g., GET /api/users) and time window (e.g., 3 seconds). Instead of 1000 transactions, send one with count = 1000 and average metrics. diff --git a/example/index.html b/example/index.html index 87faebf..6f5b6cb 100644 --- a/example/index.html +++ b/example/index.html @@ -84,64 +84,6 @@ font-size: 15px; color:inherit; } - - .performance-demo { - border-radius: 4px; - } - - .performance-demo button { - padding: 8px 16px; - padding-right: 32px; - background: #4979E4; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - position: relative; - min-width: 200px; - text-align: left; - } - - .performance-demo button:hover { - background: #4869d2; - } - - .performance-demo button[disabled] { - background: #4979E4; - opacity: 0.7; - cursor: not-allowed; - } - - .performance-demo button .spinner { - display: none; - width: 10px; - height: 10px; - border: 2px solid #fff; - border-bottom-color: transparent; - border-radius: 50%; - animation: rotation 1s linear infinite; - position: absolute; - right: 10px; - top: 50%; - margin-top: -5px; - } - - #sampleRate, #batchInterval, #generateTransactions { - margin-bottom: 10px; - } - - .performance-demo button[disabled] .spinner { - display: block; - } - - @keyframes rotation { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } @@ -210,55 +152,7 @@

Test console catcher



-
-

Performance Monitoring Demo

-
- -
-
-
-

Performance Monitoring Settings Demo

-
-
- - - - -
-
- -
- Sampled: 0 / Total: 0 -
-
- Last batch sent: Never -
-
-
-
- - +

Test Vue integration: $root

@@ -302,7 +196,7 @@

Test Vue integration: <test-component>

el: tag, instance: new Editor(), classProto: Editor, - longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', + longText: 'Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.Upvoting this, given it\'s simplicity. In my use case I need to check all the attributes in an object for dodgy values-NaNs, nulls, undefined (they were points on a graph and these values prevented the graph from drawing). To get the value instead of the name, in the loop you would just do obj[someVariable]. Perhaps the reason it was downvoted so much is because it is not recursive. So this would not be an adequate solution if you have a highly structured object.', } }, props: { @@ -340,120 +234,7 @@

Test Vue integration: <test-component>

context: { rootContextSample: '12345' }, - performance: true }) - diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index e9a9ea4..e34ac40 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -20,9 +20,14 @@ const DEFAULT_SAMPLE_RATE = 1.0; * Default threshold in milliseconds for filtering out short transactions * Transactions shorter than this duration will not be sent */ - const DEFAULT_THRESHOLD_MS = 20; +/** + * Default threshold in milliseconds for critical transactions + * Transactions longer than this duration will always be sent regardless of sampling + */ +const DEFAULT_CRITICAL_DURATION_THRESHOLD_MS = 500; + /** * Class for managing performance monitoring */ @@ -48,7 +53,8 @@ export default class PerformanceMonitoring { * @param debug - Debug mode flag * @param sampleRate - Sample rate for performance data (0.0 to 1.0). Must be between 0 and 1. * @param batchInterval - Interval between batch sends in milliseconds. Defaults to 3000ms. - * @param thresholdMs - Minimum duration threshold in milliseconds. Transactions shorter than this will be filtered out. Defaults to 1000ms. + * @param thresholdMs - Minimum duration threshold in milliseconds. Transactions shorter than this will be filtered out. Defaults to 20ms. + * @param criticalDurationThresholdMs - Duration threshold for critical transactions. Transactions longer than this will always be sent. Defaults to 500ms. */ constructor( private readonly transport: Socket, @@ -56,7 +62,8 @@ export default class PerformanceMonitoring { private readonly debug: boolean = false, sampleRate: number = DEFAULT_SAMPLE_RATE, private readonly batchInterval: number = DEFAULT_BATCH_INTERVAL, - private readonly thresholdMs: number = DEFAULT_THRESHOLD_MS + private readonly thresholdMs: number = DEFAULT_THRESHOLD_MS, + private readonly criticalDurationThresholdMs: number = DEFAULT_CRITICAL_DURATION_THRESHOLD_MS ) { if (sampleRate < 0 || sampleRate > 1) { console.error('Performance monitoring sample rate must be between 0 and 1'); @@ -83,7 +90,6 @@ export default class PerformanceMonitoring { * Starts a new transaction * * @param name - Transaction name - * @param tags - Optional tags for the transaction * @param severity - Severity of the transaction * @returns Transaction object */ @@ -96,6 +102,7 @@ export default class PerformanceMonitoring { return new Transaction(data, this, { sampleRate: this.sampleRate, thresholdMs: this.thresholdMs, + criticalDurationThresholdMs: this.criticalDurationThresholdMs }); } diff --git a/src/modules/performance/transaction.ts b/src/modules/performance/transaction.ts index dcddc0b..33e2aea 100644 --- a/src/modules/performance/transaction.ts +++ b/src/modules/performance/transaction.ts @@ -73,6 +73,7 @@ export class Transaction { /** * Finishes the transaction and queues it for sending if: * - It's a failure or has critical severity + * - Its duration is above the critical threshold * - Its duration is above the threshold and passes sampling * * @param status - Status of the transaction ('success' or 'failure'). Defaults to 'success' @@ -85,7 +86,12 @@ export class Transaction { // Always send if it's a failure or critical severity if (status === 'failure' || this.severity === 'critical') { this.queueForSending(); + return; + } + // Always send if duration exceeds critical threshold + if (this.duration >= this.config.criticalDurationThresholdMs) { + this.queueForSending(); return; } diff --git a/src/modules/performance/types.ts b/src/modules/performance/types.ts index da660ed..1e8fbfd 100644 --- a/src/modules/performance/types.ts +++ b/src/modules/performance/types.ts @@ -43,4 +43,5 @@ export interface AggregatedSpan { export interface PerformanceMonitoringConfig { sampleRate: number; thresholdMs: number; + criticalDurationThresholdMs: number; } diff --git a/src/types/hawk-initial-settings.ts b/src/types/hawk-initial-settings.ts index 27cdd86..d264c50 100644 --- a/src/types/hawk-initial-settings.ts +++ b/src/types/hawk-initial-settings.ts @@ -99,9 +99,16 @@ export interface PerformanceMonitoringConfig { batchInterval: number; /** - * Interval between batch sends in milliseconds - * - * @default 3000 + * Minimum duration threshold in milliseconds. + * Transactions shorter than this will be filtered out. + * @default 100 */ thresholdMs: number; + + /** + * Maximum duration threshold in milliseconds. + * Transactions with duration greather than this will not be samples out. + * @default 500 + */ + criticalDurationThresholdMs: number; } From c4f0094ce2f3b3cd20c9305a385b712e700051a2 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 22:55:17 +0300 Subject: [PATCH 34/37] Refactor performance monitoring code by adding missing commas for consistency and improving documentation clarity. Update comments to enhance understanding of methods and parameters. --- src/modules/performance/index.ts | 12 +++++++----- src/modules/performance/span.ts | 2 +- src/modules/performance/transaction.ts | 2 ++ src/types/hawk-initial-settings.ts | 6 ++++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index e34ac40..4682c76 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -102,7 +102,7 @@ export default class PerformanceMonitoring { return new Transaction(data, this, { sampleRate: this.sampleRate, thresholdMs: this.thresholdMs, - criticalDurationThresholdMs: this.criticalDurationThresholdMs + criticalDurationThresholdMs: this.criticalDurationThresholdMs, }); } @@ -202,13 +202,13 @@ export default class PerformanceMonitoring { /** * Aggregates spans from multiple transactions into statistical summaries * Groups spans by name across all transactions in the group - * + * * @param transactions - Transactions containing spans to aggregate * @returns Array of aggregated spans with statistical metrics */ private aggregateSpans(transactions: Transaction[]): AggregatedSpan[] { const spansByName = new Map(); - + // Group spans by name across all transactions transactions.forEach(transaction => { transaction.spans.forEach(span => { @@ -224,7 +224,7 @@ export default class PerformanceMonitoring { const durations = spans.map(s => s.duration ?? 0).sort((a, b) => a - b); const startTimes = spans.map(s => s.startTime ?? 0); const endTimes = spans.map((s, index) => s.endTime ?? startTimes[index]); - + // Calculate failure rate for spans const failureCount = spans.filter(s => s.status === 'failure').length; const failureRate = (failureCount / spans.length) * 100; @@ -237,13 +237,14 @@ export default class PerformanceMonitoring { p50duration: this.percentile(durations, 50), p95duration: this.percentile(durations, 95), maxDuration: Math.max(...durations), - failureRate + failureRate, }; }); } /** * Calculates the percentile value from a sorted array of numbers + * * @param sortedValues - Sorted array of numbers * @param p - Percentile to calculate (e.g., 50 for median, 95 for 95th percentile) * @returns Percentile value @@ -256,6 +257,7 @@ export default class PerformanceMonitoring { /** * Calculates the average value from an array of numbers + * * @param values - Array of numbers * @returns Average value */ diff --git a/src/modules/performance/span.ts b/src/modules/performance/span.ts index 4a35af5..a55e9b5 100644 --- a/src/modules/performance/span.ts +++ b/src/modules/performance/span.ts @@ -31,7 +31,7 @@ export class Span { /** * Finishes the span and calculates its duration - * + * * @param status - Status of the span ('success' or 'failure'). Defaults to 'success' */ public finish(status: 'success' | 'failure' = 'success'): void { diff --git a/src/modules/performance/transaction.ts b/src/modules/performance/transaction.ts index 33e2aea..cb4847e 100644 --- a/src/modules/performance/transaction.ts +++ b/src/modules/performance/transaction.ts @@ -86,12 +86,14 @@ export class Transaction { // Always send if it's a failure or critical severity if (status === 'failure' || this.severity === 'critical') { this.queueForSending(); + return; } // Always send if duration exceeds critical threshold if (this.duration >= this.config.criticalDurationThresholdMs) { this.queueForSending(); + return; } diff --git a/src/types/hawk-initial-settings.ts b/src/types/hawk-initial-settings.ts index d264c50..70d410a 100644 --- a/src/types/hawk-initial-settings.ts +++ b/src/types/hawk-initial-settings.ts @@ -99,15 +99,17 @@ export interface PerformanceMonitoringConfig { batchInterval: number; /** - * Minimum duration threshold in milliseconds. + * Minimum duration threshold in milliseconds. * Transactions shorter than this will be filtered out. + * * @default 100 */ thresholdMs: number; /** - * Maximum duration threshold in milliseconds. + * Maximum duration threshold in milliseconds. * Transactions with duration greather than this will not be samples out. + * * @default 500 */ criticalDurationThresholdMs: number; From e5a32cae12a12f0ba661bf24d9f01793591b3ebb Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 23:00:26 +0300 Subject: [PATCH 35/37] Add retries --- src/modules/performance/index.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index 4682c76..0198325 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -28,6 +28,11 @@ const DEFAULT_THRESHOLD_MS = 20; */ const DEFAULT_CRITICAL_DURATION_THRESHOLD_MS = 500; +/** + * Maximum number of retries for sending performance data + */ +const MAX_SEND_RETRIES = 3; + /** * Class for managing performance monitoring */ @@ -47,6 +52,11 @@ export default class PerformanceMonitoring { */ private readonly sampleRate: number; + /** + * Retry counter for failed send attempts + */ + private sendRetries: Map = new Map(); + /** * @param transport - Transport instance for sending data * @param token - Integration token @@ -147,9 +157,25 @@ export default class PerformanceMonitoring { const aggregatedTransactions = this.aggregateTransactions(transactions); await this.sendPerformanceData(aggregatedTransactions); + + // Clear retry counters for successful transactions + transactions.forEach(tx => this.sendRetries.delete(tx.id)); } catch (error) { - // todo: add repeats limit - this.sendQueue.push(...transactions); + // Add transactions back to queue with retry limit + const retriedTransactions = transactions.filter(tx => { + const retryCount = (this.sendRetries.get(tx.id) || 0) + 1; + this.sendRetries.set(tx.id, retryCount); + + const shouldRetry = retryCount <= MAX_SEND_RETRIES; + + if (!shouldRetry && this.debug) { + log(`Performance Monitoring: Transaction ${tx.name} (${tx.id}) exceeded retry limit and will be dropped`, 'warn'); + } + + return shouldRetry; + }); + + this.sendQueue.push(...retriedTransactions); if (this.debug) { log('Failed to send performance data', 'error', error); From 17c44f5fd3062fdcbaa521244de7df15d2801472 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 23:06:19 +0300 Subject: [PATCH 36/37] Enhance performance monitoring by adding detailed documentation for Span and Transaction classes, including descriptions for properties and methods. Remove unused types from performance module and update imports for better clarity and organization. --- src/modules/performance/span.ts | 39 ++++++++++++- src/modules/performance/transaction.ts | 40 ++++++++++++- src/modules/performance/types.ts | 47 --------------- src/types/transaction.ts | 81 +++++++++++++++++++++++++- 4 files changed, 155 insertions(+), 52 deletions(-) delete mode 100644 src/modules/performance/types.ts diff --git a/src/modules/performance/span.ts b/src/modules/performance/span.ts index a55e9b5..3d8b88a 100644 --- a/src/modules/performance/span.ts +++ b/src/modules/performance/span.ts @@ -1,8 +1,17 @@ import { id } from '../../utils/id'; import { getTimestamp } from '../../utils/get-timestamp'; - +/** + * Interface for data required to construct a Span + */ interface SpanConstructionData { + /** + * ID of the transaction this span belongs to + */ transactionId: string; + + /** + * Name of the span + */ name: string; } @@ -10,13 +19,39 @@ interface SpanConstructionData { * Class representing a span of work within a transaction */ export class Span { + /** + * Unique identifier for this span + */ public readonly id: string = id(); + + /** + * ID of the transaction this span belongs to + */ public readonly transactionId: string; + + /** + * Name of the span + */ public readonly name: string; + + /** + * Timestamp when the span started + */ public readonly startTime: number = getTimestamp(); + + /** + * Timestamp when the span ended + */ public endTime?: number; + + /** + * Duration of the span in milliseconds + */ public duration?: number; - public readonly metadata?: Record; + + /** + * Status indicating whether the span completed successfully or failed + */ public status: 'success' | 'failure' = 'success'; /** diff --git a/src/modules/performance/transaction.ts b/src/modules/performance/transaction.ts index cb4847e..26d18d9 100644 --- a/src/modules/performance/transaction.ts +++ b/src/modules/performance/transaction.ts @@ -2,7 +2,7 @@ import { getTimestamp } from '../../utils/get-timestamp'; import type PerformanceMonitoring from '.'; import { Span } from './span'; import { id } from '../../utils/id'; -import type { PerformanceMonitoringConfig } from './types'; +import { PerformanceMonitoringConfig } from 'src/types/hawk-initial-settings'; /** * Interface representing data needed to construct a Transaction @@ -21,17 +21,53 @@ export interface TransactionConstructionData { severity: 'default' | 'critical'; } +/** + * Class representing a transaction that can contain multiple spans + */ /** * Class representing a transaction that can contain multiple spans */ export class Transaction { + /** + * Unique identifier for this transaction + */ public readonly id: string = id(); + + /** + * Name of the transaction + */ public readonly name: string; + + /** + * Timestamp when the transaction started + */ public readonly startTime: number = getTimestamp(); + + /** + * Timestamp when the transaction ended + */ public endTime?: number; + + /** + * Duration of the transaction in milliseconds + */ public duration?: number; + + /** + * Array of spans contained within this transaction + */ public readonly spans: Span[] = []; + + /** + * Status indicating whether the transaction completed successfully or failed + */ public finishStatus: 'success' | 'failure' = 'success'; + + /** + * Severity level of the transaction + * - 'default': Normal transaction that might be sampled out + * - 'critical': High priority transaction that will be sent regardless of sampling rate + */ private severity: 'default' | 'critical' = 'default'; /** @@ -44,7 +80,7 @@ export class Transaction { constructor( data: TransactionConstructionData, private readonly performance: PerformanceMonitoring, - private readonly config: PerformanceMonitoringConfig + private readonly config: Omit ) { this.name = data.name; this.severity = data.severity; diff --git a/src/modules/performance/types.ts b/src/modules/performance/types.ts deleted file mode 100644 index 1e8fbfd..0000000 --- a/src/modules/performance/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -export interface Transaction { - id: string; - severity: 'default' | 'critical'; - name: string; - startTime: number; - endTime: number; - duration: number; - status: 'success' | 'failure'; - spans: Span[]; -} - -export interface AggregatedTransaction { - aggregationId: string; - name: string; - avgStartTime: number; - minStartTime: number; - maxEndTime: number; - p50duration: number; - p95duration: number; - maxDuration: number; - count: number; - aggregatedSpans: AggregatedSpan[]; -} - -export interface Span { - id: string; - name: string; - startTime: number; - endTime: number; - duration: number; -} - -export interface AggregatedSpan { - aggregationId: string; - name: string; - minStartTime: number; - maxEndTime: number; - p50duration: number; - p95duration: number; - maxDuration: number; -} - -export interface PerformanceMonitoringConfig { - sampleRate: number; - thresholdMs: number; - criticalDurationThresholdMs: number; -} diff --git a/src/types/transaction.ts b/src/types/transaction.ts index ae783a0..b6414bb 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -1,25 +1,104 @@ +/** + * Interface representing aggregated statistics for a group of transactions with the same name + */ export interface AggregatedTransaction { + /** + * Unique identifier for this aggregation, combining name and timestamp + */ aggregationId: string; + + /** + * Name of the transactions being aggregated + */ name: string; + + /** + * Average start time across all transactions in the group + */ avgStartTime: number; + + /** + * Earliest start time among all transactions in the group + */ minStartTime: number; + + /** + * Latest end time among all transactions in the group + */ maxEndTime: number; + + /** + * 50th percentile (median) duration across all transactions + */ p50duration: number; + + /** + * 95th percentile duration across all transactions + */ p95duration: number; + + /** + * Maximum duration among all transactions + */ maxDuration: number; + + /** + * Total number of transactions in this group + */ count: number; + + /** + * Percentage of transactions that failed + */ failureRate: number; + + /** + * Array of aggregated statistics for spans within these transactions + */ aggregatedSpans: AggregatedSpan[]; } +/** + * Interface representing aggregated statistics for a group of spans with the same name + */ export interface AggregatedSpan { + /** + * Unique identifier for this span aggregation + */ aggregationId: string; + + /** + * Name of the spans being aggregated + */ name: string; + + /** + * Earliest start time among all spans in the group + */ minStartTime: number; + + /** + * Latest end time among all spans in the group + */ maxEndTime: number; + + /** + * 50th percentile (median) duration across all spans + */ p50duration: number; + + /** + * 95th percentile duration across all spans + */ p95duration: number; + + /** + * Maximum duration among all spans + */ maxDuration: number; + + /** + * Percentage of spans that failed + */ failureRate: number; } - From 61649a966f7d6b03bda81831a83e460ede4b3976 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 16 Mar 2025 23:07:27 +0300 Subject: [PATCH 37/37] Refactor consoleCatcher and performance monitoring code for improved readability. Adjust formatting for better alignment and clarity, and update import statements for consistency. --- src/addons/consoleCatcher.ts | 4 +++- src/modules/performance/index.ts | 11 ++++++----- src/modules/performance/transaction.ts | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index fc8fa13..650e9ff 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -63,7 +63,8 @@ const createConsoleCatcher = (): { const oldFunction = window.console[method].bind(window.console); window.console[method] = function (...args: unknown[]): void { - const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; + const stack = new Error().stack?.split('\n').slice(2) + .join('\n') || ''; const logEvent: ConsoleLogEvent = { method, @@ -93,4 +94,5 @@ const createConsoleCatcher = (): { }; const consoleCatcher = createConsoleCatcher(); + export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } = consoleCatcher; diff --git a/src/modules/performance/index.ts b/src/modules/performance/index.ts index 0198325..2ea7289 100644 --- a/src/modules/performance/index.ts +++ b/src/modules/performance/index.ts @@ -157,24 +157,25 @@ export default class PerformanceMonitoring { const aggregatedTransactions = this.aggregateTransactions(transactions); await this.sendPerformanceData(aggregatedTransactions); - + // Clear retry counters for successful transactions transactions.forEach(tx => this.sendRetries.delete(tx.id)); } catch (error) { // Add transactions back to queue with retry limit const retriedTransactions = transactions.filter(tx => { const retryCount = (this.sendRetries.get(tx.id) || 0) + 1; + this.sendRetries.set(tx.id, retryCount); - + const shouldRetry = retryCount <= MAX_SEND_RETRIES; - + if (!shouldRetry && this.debug) { log(`Performance Monitoring: Transaction ${tx.name} (${tx.id}) exceeded retry limit and will be dropped`, 'warn'); } - + return shouldRetry; }); - + this.sendQueue.push(...retriedTransactions); if (this.debug) { diff --git a/src/modules/performance/transaction.ts b/src/modules/performance/transaction.ts index 26d18d9..70c29a4 100644 --- a/src/modules/performance/transaction.ts +++ b/src/modules/performance/transaction.ts @@ -2,7 +2,7 @@ import { getTimestamp } from '../../utils/get-timestamp'; import type PerformanceMonitoring from '.'; import { Span } from './span'; import { id } from '../../utils/id'; -import { PerformanceMonitoringConfig } from 'src/types/hawk-initial-settings'; +import type { PerformanceMonitoringConfig } from 'src/types/hawk-initial-settings'; /** * Interface representing data needed to construct a Transaction