From 94d67b753b1b15b6669cd82f6a00231f4afe4963 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 27 Jun 2023 17:37:32 +0200 Subject: [PATCH 01/13] URL parsers --- .../model/http/BacktraceReportSubmission.ts | 18 +---- .../model/http/SubmissionUrlInformation.ts | 71 +++++++++++++++++++ packages/sdk-core/src/model/http/index.ts | 1 + .../modules/metrics/MetricsUrlInformation.ts | 64 +++++++++++++++++ .../http/submissionUrlGenerationTests.spec.ts | 37 ++++++++++ .../sdk-core/tests/http/tokenTests.spec.ts | 24 +++++++ .../sdk-core/tests/http/universeTests.spec.ts | 26 +++++++ 7 files changed, 225 insertions(+), 16 deletions(-) create mode 100644 packages/sdk-core/src/model/http/SubmissionUrlInformation.ts create mode 100644 packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts create mode 100644 packages/sdk-core/tests/http/submissionUrlGenerationTests.spec.ts create mode 100644 packages/sdk-core/tests/http/tokenTests.spec.ts create mode 100644 packages/sdk-core/tests/http/universeTests.spec.ts diff --git a/packages/sdk-core/src/model/http/BacktraceReportSubmission.ts b/packages/sdk-core/src/model/http/BacktraceReportSubmission.ts index 124a5acd..b3a29352 100644 --- a/packages/sdk-core/src/model/http/BacktraceReportSubmission.ts +++ b/packages/sdk-core/src/model/http/BacktraceReportSubmission.ts @@ -2,29 +2,15 @@ import { BacktraceAttachment } from '../attachment'; import { BacktraceConfiguration } from '../configuration/BacktraceConfiguration'; import { BacktraceData } from '../data/BacktraceData'; import { BacktraceRequestHandler } from './BacktraceRequestHandler'; +import { SubmissionUrlInformation } from './SubmissionUrlInformation'; export class BacktraceReportSubmission { private readonly _submissionUrl: string; constructor(options: BacktraceConfiguration, private readonly _requestHandler: BacktraceRequestHandler) { - this._submissionUrl = this.generateReportSubmissionUrl(options.url, options.token); + this._submissionUrl = SubmissionUrlInformation.toJsonReportSubmissionUrl(options.url, options.token); } public send(data: BacktraceData, attachments: BacktraceAttachment[]) { return this._requestHandler.postError(this._submissionUrl, data, attachments); } - - private generateReportSubmissionUrl(url: string, token?: string) { - // if the token doesn't exist - use URL - if (!token) { - return url; - } - - // if the URL has token in the URL, the user probably added a token once again - // in this case, don't do anything - if (url.indexOf(token) !== -1) { - return url; - } - - return new URL(`/post?format=json&token=${token}`, url).href; - } } diff --git a/packages/sdk-core/src/model/http/SubmissionUrlInformation.ts b/packages/sdk-core/src/model/http/SubmissionUrlInformation.ts new file mode 100644 index 00000000..9aa29152 --- /dev/null +++ b/packages/sdk-core/src/model/http/SubmissionUrlInformation.ts @@ -0,0 +1,71 @@ +export class SubmissionUrlInformation { + private static SUBMIT_PREFIX = 'submit.backtrace.io/'; + + /** + * Convert url/token from credentials to JSON submission URL + * @param url credentials URL + * @param token credentials token + * @returns JSON submissionURL + */ + public static toJsonReportSubmissionUrl(url: string, token?: string): string { + // if the token doesn't exist - use URL + if (!token) { + return url; + } + + // if the url points to submit, we should always use it without any modifications + if (url.includes(this.SUBMIT_PREFIX)) { + return url; + } + + // if the URL has token in the URL, the user probably added a token once again + // in this case, don't do anything + if (url.indexOf(token) !== -1) { + return url; + } + + return new URL(`/post?format=json&token=${token}`, url).href; + } + + /** + * Find the universe based on the submission URL + * @param submissionUrl submission URL + * @returns universe name + */ + public static findUniverse(submissionUrl: string): string { + const submitIndex = submissionUrl.indexOf(this.SUBMIT_PREFIX); + if (submitIndex !== -1) { + const universeStartIndex = submitIndex + this.SUBMIT_PREFIX.length; + const endOfUniverseName = submissionUrl.indexOf('/', universeStartIndex); + return submissionUrl.substring(universeStartIndex, endOfUniverseName); + } + // the universe name should be available in the hostname + // for example abc.sp.backtrace.io or zyx.in.backtrace.io or foo.backtrace.io + const hostname = new URL(submissionUrl).host; + const endOfUniverseName = hostname.indexOf('.'); + + return hostname.substring(0, endOfUniverseName); + } + + public static findToken(submissionUrl: string): string | undefined { + const tokenLength = 64; + const submitIndex = submissionUrl.indexOf(this.SUBMIT_PREFIX); + if (submitIndex !== -1) { + const submissionUrlParts = submissionUrl.split('/'); + // submit format URL + // submit.backtrace.io/universe/token/format + // by spliting the submission URL by `/` and dropping the last + // part of the URL, the last element on the list is the token. + return submissionUrlParts[submissionUrlParts.length - 2]; + } + + const tokenQueryParameter = 'token='; + const tokenQueryParameterIndex = submissionUrl.indexOf(tokenQueryParameter); + if (tokenQueryParameterIndex === -1) { + return undefined; + } + + const tokenStartIndex = tokenQueryParameterIndex + tokenQueryParameter.length; + return submissionUrl.substring(tokenStartIndex, tokenStartIndex + tokenLength); + } +} diff --git a/packages/sdk-core/src/model/http/index.ts b/packages/sdk-core/src/model/http/index.ts index 51cf7152..d268daf7 100644 --- a/packages/sdk-core/src/model/http/index.ts +++ b/packages/sdk-core/src/model/http/index.ts @@ -3,3 +3,4 @@ export * from './common/ConnectionError'; export * from './model/BacktraceSubmissionResponse'; export * from './model/BacktraceSubmissionResult'; export * from './model/BacktraceSubmissionStatus'; +export * from './SubmissionUrlInformation'; diff --git a/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts b/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts new file mode 100644 index 00000000..489f2ed3 --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts @@ -0,0 +1,64 @@ +import { SubmissionUrlInformation } from '../../model/http'; + +export class MetricsUrlInformation { + public static generateSummedEventsUrl( + hostname: string, + submissionUrl: string, + credentialsToken?: string, + ): string | undefined { + const submissionInformation = this.findSubmissionInformation(submissionUrl, credentialsToken); + if (!submissionInformation) { + return undefined; + } + return this.generateEventsServiceUrl( + hostname, + 'summed-events', + submissionInformation.universe, + submissionInformation.token, + ); + } + + public static generateUniqueEventsUrl( + hostname: string, + submissionUrl: string, + credentialsToken?: string, + ): string | undefined { + const submissionInformation = this.findSubmissionInformation(submissionUrl, credentialsToken); + if (!submissionInformation) { + return undefined; + } + + return this.generateEventsServiceUrl( + hostname, + 'unique-events', + submissionInformation.universe, + submissionInformation.token, + ); + } + + private static generateEventsServiceUrl( + hostname: string, + eventServiceName: string, + universe: string, + token: string, + ): string { + return new URL(`/api/${eventServiceName}/submit?universe=${universe}&token=${token}`, hostname).toString(); + } + + private static findSubmissionInformation( + submissionUrl: string, + token?: string, + ): { universe: string; token: string } | undefined { + const universe = SubmissionUrlInformation.findUniverse(submissionUrl); + if (!universe) { + return undefined; + } + + token = token ?? SubmissionUrlInformation.findToken(submissionUrl); + + if (!token) { + return undefined; + } + return { universe, token }; + } +} diff --git a/packages/sdk-core/tests/http/submissionUrlGenerationTests.spec.ts b/packages/sdk-core/tests/http/submissionUrlGenerationTests.spec.ts new file mode 100644 index 00000000..5b2c7404 --- /dev/null +++ b/packages/sdk-core/tests/http/submissionUrlGenerationTests.spec.ts @@ -0,0 +1,37 @@ +import { SubmissionUrlInformation } from '../../src/model/http'; +describe('Submission Url generation tests', () => { + describe('Submit', () => { + const sampleSubmitUrl = `https://submit.backtrace.io/name/000000000000a1eb7ae344f6e002de2e20c81fbdedf6991c2f3bb45b11111111/json`; + it('Should use submit url from the configuration options', () => { + expect(SubmissionUrlInformation.toJsonReportSubmissionUrl(sampleSubmitUrl)).toBe(sampleSubmitUrl); + }); + + it(`Shouldnt mix token with the submission url`, () => { + expect(SubmissionUrlInformation.toJsonReportSubmissionUrl(sampleSubmitUrl, '123')).toBe(sampleSubmitUrl); + }); + }); + + describe('Direct URL', () => { + const hostname = `https://instance.sp.backtrace.io`; + const token = `000000000000a1eb7ae344f6e002de2e20c81fbdedf6991c2f3bb45b11111111`; + const fullUrl = `${hostname}/post?format=json&token=${token}`; + it('Should use the direct url if the token is not available', () => { + expect(SubmissionUrlInformation.toJsonReportSubmissionUrl(fullUrl)).toBe(fullUrl); + }); + + it(`Shouldn't mix token with the submission url if the token is already there`, () => { + expect(SubmissionUrlInformation.toJsonReportSubmissionUrl(fullUrl, token)).toBe(fullUrl); + }); + + it(`Should generate a full url if the token and instance are passed separated`, () => { + expect(SubmissionUrlInformation.toJsonReportSubmissionUrl(hostname, token)).toBe(fullUrl); + }); + + it(`Should override the token in the submission url`, () => { + const testedToken = '111111110000000000001111111100000000000020c81fbdedf6991c2f3bb45b'; + const expectedUrl = `${hostname}/post?format=json&token=${testedToken}`; + + expect(SubmissionUrlInformation.toJsonReportSubmissionUrl(fullUrl, testedToken)).toBe(expectedUrl); + }); + }); +}); diff --git a/packages/sdk-core/tests/http/tokenTests.spec.ts b/packages/sdk-core/tests/http/tokenTests.spec.ts new file mode 100644 index 00000000..fdc0398b --- /dev/null +++ b/packages/sdk-core/tests/http/tokenTests.spec.ts @@ -0,0 +1,24 @@ +import { SubmissionUrlInformation } from '../../src/model/http'; + +describe('Token tests', () => { + const testedToken = '000000000000a1eb7ae344f6e002de2e20c81fbdedf6991c2f3bb45b11111111'; + describe('Submit', () => { + const sampleSubmitUrl = `https://submit.backtrace.io/test/${testedToken}/json`; + + it('Should correctly find the universe name', () => { + expect(SubmissionUrlInformation.findToken(sampleSubmitUrl)).toBe(testedToken); + }); + }); + + describe('Direct', () => { + it(`Should return undefined if the url doesn't contain the submission token`, () => { + expect(SubmissionUrlInformation.findToken(`https://foo.sp.backtrace.io`)).toBeUndefined(); + }); + + it(`Should return token from the direct url`, () => { + expect( + SubmissionUrlInformation.findToken(`https://foo.sp.backtrace.io/post?format=json&token=${testedToken}`), + ).toBe(testedToken); + }); + }); +}); diff --git a/packages/sdk-core/tests/http/universeTests.spec.ts b/packages/sdk-core/tests/http/universeTests.spec.ts new file mode 100644 index 00000000..578ff0a4 --- /dev/null +++ b/packages/sdk-core/tests/http/universeTests.spec.ts @@ -0,0 +1,26 @@ +import { SubmissionUrlInformation } from '../../src/model/http'; +describe('Universe tests', () => { + const testedUniverseName = 'foo-bar-baz'; + describe('Submit', () => { + const sampleSubmitUrl = `https://submit.backtrace.io/${testedUniverseName}/000000000000a1eb7ae344f6e002de2e20c81fbdedf6991c2f3bb45b11111111/json`; + + it('Should correctly find the universe name', () => { + expect(SubmissionUrlInformation.findUniverse(sampleSubmitUrl)).toBe(testedUniverseName); + }); + }); + + describe('Direct', () => { + const testedBacktraceDomainPrefixes = ['', '.sp', '.in']; + for (const backtracePrefix of testedBacktraceDomainPrefixes) { + it(`Should correctly find the universe name with prefix ${backtracePrefix}`, () => { + const sampleDirectUrl = `https://${testedUniverseName}${backtracePrefix}.backtrace.io`; + expect(SubmissionUrlInformation.findUniverse(sampleDirectUrl)).toBe(testedUniverseName); + }); + } + + it('Should correctly find the universe in the direct url with the token', () => { + const sampleDirectUrl = `https://${testedUniverseName}.sp.backtrace.io/post?format=json&token=000000000000a1eb7ae344f6e002de2e20c81fbdedf6991c2f3bb45b11111111`; + expect(SubmissionUrlInformation.findUniverse(sampleDirectUrl)).toBe(testedUniverseName); + }); + }); +}); From 9ca101cf8b93467a6b45ba509bc98e9ae2a12853 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Tue, 27 Jun 2023 17:41:50 +0200 Subject: [PATCH 02/13] Command line attribute provider --- .../src/attributes/ApplicationInformationAttributeProvider.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/node/src/attributes/ApplicationInformationAttributeProvider.ts b/packages/node/src/attributes/ApplicationInformationAttributeProvider.ts index c784bea2..df4c3c31 100644 --- a/packages/node/src/attributes/ApplicationInformationAttributeProvider.ts +++ b/packages/node/src/attributes/ApplicationInformationAttributeProvider.ts @@ -50,6 +50,10 @@ export class ApplicationInformationAttributeProvider implements BacktraceAttribu private generateDefaultApplicationSearchPaths() { const possibleSourcePaths = [process.cwd()]; + const potentialCommandLineStartupFile = process.argv[1]; + if (potentialCommandLineStartupFile && fs.existsSync(potentialCommandLineStartupFile)) { + possibleSourcePaths.unshift(potentialCommandLineStartupFile); + } if (require.main?.path) { possibleSourcePaths.unshift(path.dirname(require.main.path)); } From 71245a0db95f2abd17d401a0a79abfbfd789754b Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Wed, 28 Jun 2023 00:37:46 +0200 Subject: [PATCH 03/13] Code review suggestions --- .../model/http/SubmissionUrlInformation.ts | 29 ++++++++++--------- .../sdk-core/tests/http/tokenTests.spec.ts | 4 +-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/sdk-core/src/model/http/SubmissionUrlInformation.ts b/packages/sdk-core/src/model/http/SubmissionUrlInformation.ts index 9aa29152..fb654d16 100644 --- a/packages/sdk-core/src/model/http/SubmissionUrlInformation.ts +++ b/packages/sdk-core/src/model/http/SubmissionUrlInformation.ts @@ -24,7 +24,10 @@ export class SubmissionUrlInformation { return url; } - return new URL(`/post?format=json&token=${token}`, url).href; + const result = new URL(`/post`, url); + result.searchParams.append('format', 'json'); + result.searchParams.append('token', token); + return result.href; } /** @@ -32,23 +35,28 @@ export class SubmissionUrlInformation { * @param submissionUrl submission URL * @returns universe name */ - public static findUniverse(submissionUrl: string): string { + public static findUniverse(submissionUrl: string): string | undefined { const submitIndex = submissionUrl.indexOf(this.SUBMIT_PREFIX); if (submitIndex !== -1) { + // submit format URL + // submit.backtrace.io/universe/token/format + // we can expect the universe name just after the hostname const universeStartIndex = submitIndex + this.SUBMIT_PREFIX.length; const endOfUniverseName = submissionUrl.indexOf('/', universeStartIndex); return submissionUrl.substring(universeStartIndex, endOfUniverseName); } // the universe name should be available in the hostname // for example abc.sp.backtrace.io or zyx.in.backtrace.io or foo.backtrace.io - const hostname = new URL(submissionUrl).host; - const endOfUniverseName = hostname.indexOf('.'); + const hostname = new URL(submissionUrl).hostname; + if (!hostname.endsWith('backtrace.io')) { + return undefined; + } + const endOfUniverseName = hostname.indexOf('.'); return hostname.substring(0, endOfUniverseName); } - public static findToken(submissionUrl: string): string | undefined { - const tokenLength = 64; + public static findToken(submissionUrl: string): string | null { const submitIndex = submissionUrl.indexOf(this.SUBMIT_PREFIX); if (submitIndex !== -1) { const submissionUrlParts = submissionUrl.split('/'); @@ -59,13 +67,8 @@ export class SubmissionUrlInformation { return submissionUrlParts[submissionUrlParts.length - 2]; } - const tokenQueryParameter = 'token='; - const tokenQueryParameterIndex = submissionUrl.indexOf(tokenQueryParameter); - if (tokenQueryParameterIndex === -1) { - return undefined; - } + const url = new URL(submissionUrl); - const tokenStartIndex = tokenQueryParameterIndex + tokenQueryParameter.length; - return submissionUrl.substring(tokenStartIndex, tokenStartIndex + tokenLength); + return url.searchParams.get('token'); } } diff --git a/packages/sdk-core/tests/http/tokenTests.spec.ts b/packages/sdk-core/tests/http/tokenTests.spec.ts index fdc0398b..8eb79851 100644 --- a/packages/sdk-core/tests/http/tokenTests.spec.ts +++ b/packages/sdk-core/tests/http/tokenTests.spec.ts @@ -11,8 +11,8 @@ describe('Token tests', () => { }); describe('Direct', () => { - it(`Should return undefined if the url doesn't contain the submission token`, () => { - expect(SubmissionUrlInformation.findToken(`https://foo.sp.backtrace.io`)).toBeUndefined(); + it(`Should return null if the url doesn't contain the submission token`, () => { + expect(SubmissionUrlInformation.findToken(`https://foo.sp.backtrace.io`)).toBeNull(); }); it(`Should return token from the direct url`, () => { From 86f8262b1213ff14da8d28f66af21708f0434b0e Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Wed, 28 Jun 2023 00:44:46 +0200 Subject: [PATCH 04/13] Metrics support --- examples/sdk/browser/index.html | 6 + examples/sdk/browser/src/index.ts | 22 ++ examples/sdk/node/src/index.ts | 24 +++ .../src/BacktraceBrowserSessionProvider.ts | 72 +++++++ packages/browser/src/BacktraceClient.ts | 3 +- .../browser/tests/client/clientTests.spec.ts | 36 ++-- .../persistentSessionProviderTests.spec.ts | 38 ++++ .../node/tests/client/clientTests.spec.ts | 30 +-- packages/sdk-core/src/BacktraceCoreClient.ts | 35 +++- packages/sdk-core/src/common/DelayHelper.ts | 22 ++ packages/sdk-core/src/common/TimeHelper.ts | 4 + packages/sdk-core/src/index.ts | 1 + .../configuration/BacktraceConfiguration.ts | 22 +- .../src/modules/metrics/BacktraceMetrics.ts | 127 ++++++++++++ .../metrics/BacktraceSessionProvider.ts | 20 ++ .../src/modules/metrics/MetricsBuilder.ts | 137 +++++++++++++ .../src/modules/metrics/MetricsQueue.ts | 7 + .../modules/metrics/MetricsSubmissionQueue.ts | 82 ++++++++ .../modules/metrics/SingleSessionProvider.ts | 24 +++ .../src/modules/metrics/model/MetricsEvent.ts | 20 ++ .../src/modules/metrics/model/SummedEvent.ts | 8 + .../src/modules/metrics/model/UniqueEvent.ts | 8 + .../tests/metrics/metricSetupTests.spec.ts | 127 ++++++++++++ .../metrics/mocks/mockSubmissionQueue.ts | 14 ++ .../tests/metrics/summedEventTests.spec.ts | 193 ++++++++++++++++++ .../tests/metrics/uniqueEventTests.spec.ts | 172 ++++++++++++++++ .../tests/mocks/BacktraceTestClient.ts | 17 +- .../sdk-core/tests/mocks/testHttpClient.ts | 6 + 28 files changed, 1219 insertions(+), 58 deletions(-) create mode 100644 packages/browser/src/BacktraceBrowserSessionProvider.ts create mode 100644 packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts create mode 100644 packages/sdk-core/src/common/DelayHelper.ts create mode 100644 packages/sdk-core/src/modules/metrics/BacktraceMetrics.ts create mode 100644 packages/sdk-core/src/modules/metrics/BacktraceSessionProvider.ts create mode 100644 packages/sdk-core/src/modules/metrics/MetricsBuilder.ts create mode 100644 packages/sdk-core/src/modules/metrics/MetricsQueue.ts create mode 100644 packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts create mode 100644 packages/sdk-core/src/modules/metrics/SingleSessionProvider.ts create mode 100644 packages/sdk-core/src/modules/metrics/model/MetricsEvent.ts create mode 100644 packages/sdk-core/src/modules/metrics/model/SummedEvent.ts create mode 100644 packages/sdk-core/src/modules/metrics/model/UniqueEvent.ts create mode 100644 packages/sdk-core/tests/metrics/metricSetupTests.spec.ts create mode 100644 packages/sdk-core/tests/metrics/mocks/mockSubmissionQueue.ts create mode 100644 packages/sdk-core/tests/metrics/summedEventTests.spec.ts create mode 100644 packages/sdk-core/tests/metrics/uniqueEventTests.spec.ts create mode 100644 packages/sdk-core/tests/mocks/testHttpClient.ts diff --git a/examples/sdk/browser/index.html b/examples/sdk/browser/index.html index c98a9ee4..5bcef440 100644 --- a/examples/sdk/browser/index.html +++ b/examples/sdk/browser/index.html @@ -31,6 +31,12 @@

Welcome to the Backtrace demo Send a message + +

If you have any questions or concerns, please contact us at

diff --git a/examples/sdk/browser/src/index.ts b/examples/sdk/browser/src/index.ts index 7132174f..9da92984 100644 --- a/examples/sdk/browser/src/index.ts +++ b/examples/sdk/browser/src/index.ts @@ -21,6 +21,8 @@ function parseNotExistingDomElement(): string { const sendErrorButton = document.getElementById('send-error') as HTMLElement; const sendMessageButton = document.getElementById('send-message') as HTMLElement; +const generateMetricButton = document.getElementById('generate-metric') as HTMLElement; +const sendMetricsButton = document.getElementById('send-metrics') as HTMLElement; async function sendHandledException() { try { @@ -40,5 +42,25 @@ async function sendMessage() { ]); } +function generateMetric() { + console.log('generate-metric click'); + if (!client.metrics) { + console.log('metrics are unavailable'); + return; + } + client.metrics.addSummedEvent('click'); +} + +function sendMetrics() { + console.log('send-metrics click'); + if (!client.metrics) { + console.log('metrics are unavailable'); + return; + } + client.metrics.send(); +} + sendErrorButton.onclick = sendHandledException; sendMessageButton.onclick = sendMessage; +generateMetricButton.onclick = generateMetric; +sendMetricsButton.onclick = sendMetrics; diff --git a/examples/sdk/node/src/index.ts b/examples/sdk/node/src/index.ts index b3c1fe32..80f8ec48 100644 --- a/examples/sdk/node/src/index.ts +++ b/examples/sdk/node/src/index.ts @@ -38,11 +38,27 @@ async function sendMessage(message: string, attributes: Record) await client.send(message, attributes); } +function addEvent(name: string, attributes: Record) { + if (!client.metrics) { + console.log('metrics are unavailable'); + return; + } + client.metrics.addSummedEvent(name, attributes); +} +function sendMetrics() { + if (!client.metrics) { + console.log('metrics are unavailable'); + return; + } + client.metrics.send(); +} function showMenu() { const menu = `Please pick one of available options:\n` + `1. Send an exception\n` + `2. Send a message\n` + + `3. Add a new summed event\n` + + `4. Send all metrics\n` + `0. Exit\n` + `Type the option number:`; reader.question(menu, async function executeUserOption(optionString: string) { @@ -59,6 +75,14 @@ function showMenu() { await sendMessage('test message', attributes); break; } + case 3: { + addEvent('Option clicked', attributes); + break; + } + case 4: { + sendMetrics(); + break; + } case 0: { reader.close(); return exit(0); diff --git a/packages/browser/src/BacktraceBrowserSessionProvider.ts b/packages/browser/src/BacktraceBrowserSessionProvider.ts new file mode 100644 index 00000000..3152fd72 --- /dev/null +++ b/packages/browser/src/BacktraceBrowserSessionProvider.ts @@ -0,0 +1,72 @@ +import { BacktraceSessionProvider, IdGenerator } from '@backtrace/sdk-core'; +import { TimeHelper } from '@backtrace/sdk-core/lib/common/TimeHelper'; + +export class BacktraceBrowserSessionProvider implements BacktraceSessionProvider { + /** + * Session persistence interval. If no event was send in the persistence interval + * the session is treaten as an old session. + */ + private readonly PERSISTENCE_INTERVAL = TimeHelper.convertSecondsToMilliseconds(30 * 60); + private readonly SESSION_LAST_ACTIVE = 'backtrace-last-active'; + private readonly SESSION_GUID = 'backtrace-guid'; + + /** + * Session last active timestamp + */ + get lastActive(): number { + return this._lastActive; + } + /** + * Determinates if the current session is new. + */ + public readonly newSession: boolean = true; + /** + * Current session Id + */ + public readonly sessionId: string = IdGenerator.uuid(); + + private _lastActive = 0; + + constructor() { + if (!window.localStorage) { + return; + } + + const lastActive = this.readLastActiveTimestamp(); + if (!lastActive || TimeHelper.now() - lastActive > this.PERSISTENCE_INTERVAL) { + this.updateLastActiveTimestamp(); + localStorage.setItem(this.SESSION_GUID, this.sessionId); + return; + } + this._lastActive = lastActive; + this.newSession = false; + this.sessionId = localStorage.getItem(this.SESSION_GUID) as string; + } + + public afterMetricsSubmission(): void { + this.updateLastActiveTimestamp(); + } + + public shouldSend(): boolean { + return document.hidden === false; + } + + private readLastActiveTimestamp(): number | undefined { + const lastActiveStringTimestamp = localStorage.getItem(this.SESSION_LAST_ACTIVE); + if (!lastActiveStringTimestamp) { + return undefined; + } + + const lastActive = parseInt(lastActiveStringTimestamp, 10); + if (isNaN(lastActive)) { + return undefined; + } + + return lastActive; + } + + private updateLastActiveTimestamp() { + this._lastActive = TimeHelper.now(); + localStorage.setItem(this.SESSION_LAST_ACTIVE, this._lastActive.toString(10)); + } +} diff --git a/packages/browser/src/BacktraceClient.ts b/packages/browser/src/BacktraceClient.ts index 8e6d65fb..c0ee6744 100644 --- a/packages/browser/src/BacktraceClient.ts +++ b/packages/browser/src/BacktraceClient.ts @@ -5,6 +5,7 @@ import { BacktraceStackTraceConverter, } from '@backtrace/sdk-core'; import { AGENT } from './agentDefinition'; +import { BacktraceBrowserSessionProvider } from './BacktraceBrowserSessionProvider'; import { BacktraceConfiguration } from './BacktraceConfiguration'; import { BacktraceClientBuilder } from './builder/BacktraceClientBuilder'; @@ -15,7 +16,7 @@ export class BacktraceClient extends BacktraceCoreClient { attributeProviders: BacktraceAttributeProvider[], stackTraceConverter: BacktraceStackTraceConverter, ) { - super(options, AGENT, handler, attributeProviders, stackTraceConverter); + super(options, AGENT, handler, attributeProviders, stackTraceConverter, new BacktraceBrowserSessionProvider()); } public static builder(options: BacktraceConfiguration): BacktraceClientBuilder { diff --git a/packages/browser/tests/client/clientTests.spec.ts b/packages/browser/tests/client/clientTests.spec.ts index ecffff00..0606afe6 100644 --- a/packages/browser/tests/client/clientTests.spec.ts +++ b/packages/browser/tests/client/clientTests.spec.ts @@ -7,26 +7,25 @@ describe('Client tests', () => { postError: jest.fn().mockResolvedValue(Promise.resolve()), }; + const defaultClientOptions = { + name: 'test', + version: '1.0.0', + url: 'https://submit.backtrace.io/foo/bar/baz', + metrics: { + enable: false, + }, + }; + let client: BacktraceClient; it('Should create a client', () => { - client = BacktraceClient.builder({ - name: 'test', - version: '1.0.0', - url: 'https://submit.backtrace.io/foo/bar/baz', - }).build(); + client = BacktraceClient.builder(defaultClientOptions).build(); expect(client).toBeDefined(); }); describe('Send tests', () => { beforeEach(() => { - client = BacktraceClient.builder({ - name: 'test', - version: '1.0.0', - url: 'https://submit.backtrace.io/foo/bar/baz', - }) - .useRequestHandler(requestHandler) - .build(); + client = BacktraceClient.builder(defaultClientOptions).useRequestHandler(requestHandler).build(); }); it(`Should not throw an error when sending a message`, async () => { expect(async () => await client.send('test')).not.toThrow(); @@ -48,9 +47,7 @@ describe('Client tests', () => { it(`Should generate an attachment list based on the client options`, async () => { const testedAttachment = new BacktraceUint8ArrayAttachment('client-add-test', new Uint8Array(0)); client = BacktraceClient.builder({ - name: 'test', - version: '1.0.0', - url: 'https://submit.backtrace.io/foo/bar/baz', + ...defaultClientOptions, attachments: [testedAttachment], }) .useRequestHandler(requestHandler) @@ -63,14 +60,7 @@ describe('Client tests', () => { it(`Should allow to add more attachments`, async () => { const testedAttachment = new BacktraceUint8ArrayAttachment('client-add-test', new Uint8Array(0)); - client = BacktraceClient.builder({ - name: 'test', - version: '1.0.0', - url: 'https://submit.backtrace.io/foo/bar/baz', - attachments: [], - }) - .useRequestHandler(requestHandler) - .build(); + client = BacktraceClient.builder(defaultClientOptions).useRequestHandler(requestHandler).build(); client.attachments.push(testedAttachment); expect(client.attachments).toBeDefined(); diff --git a/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts b/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts new file mode 100644 index 00000000..d75014ae --- /dev/null +++ b/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts @@ -0,0 +1,38 @@ +import { TimeHelper } from '@backtrace/sdk-core/lib/common/TimeHelper'; +import { BacktraceBrowserSessionProvider } from '../../src/BacktraceBrowserSessionProvider'; +describe('Session provider tests', () => { + it('Should generate a new uuid on new session', () => { + const sessionProvider = new BacktraceBrowserSessionProvider(); + + expect(sessionProvider.sessionId).toBeDefined(); + }); + + it('Should reuse the same sessionId', () => { + const sessionProvider1 = new BacktraceBrowserSessionProvider(); + const sessionProvider2 = new BacktraceBrowserSessionProvider(); + expect(sessionProvider1.sessionId).toEqual(sessionProvider2.sessionId); + }); + + it('Should generate a new sessionId if the lastActive timestamp is greater than x', () => { + const fakeId = 'test'; + const sessionProvider = new BacktraceBrowserSessionProvider(); + localStorage.setItem('backtrace-last-active', new Date(2010, 1, 1, 1, 1, 1, 1).getTime().toString(10)); + localStorage.setItem('backtrace-guid', fakeId); + expect(sessionProvider.sessionId).toBeDefined(); + expect(sessionProvider.sessionId).not.toEqual(fakeId); + }); + + it('Should update timestamp', () => { + const timestamp = Date.now(); + jest.spyOn(TimeHelper, 'now').mockImplementation(() => { + return timestamp; + }); + + localStorage.setItem('backtrace-last-active', new Date(2010, 1, 1, 1, 1, 1, 1).getTime().toString(10)); + const sessionProvider = new BacktraceBrowserSessionProvider(); + + sessionProvider.afterMetricsSubmission(); + + expect(sessionProvider.lastActive).toEqual(timestamp); + }); +}); diff --git a/packages/node/tests/client/clientTests.spec.ts b/packages/node/tests/client/clientTests.spec.ts index 1dd327a6..36318377 100644 --- a/packages/node/tests/client/clientTests.spec.ts +++ b/packages/node/tests/client/clientTests.spec.ts @@ -8,22 +8,22 @@ describe('Client tests', () => { postError: jest.fn().mockResolvedValue(Promise.resolve()), }; + const defaultClientOptions = { + url: 'https://submit.backtrace.io/foo/bar/baz', + metrics: { + enable: false, + }, + }; let client: BacktraceClient; it('Should create a client', () => { - client = BacktraceClient.builder({ - url: 'https://submit.backtrace.io/foo/bar/baz', - }).build(); + client = BacktraceClient.builder(defaultClientOptions).build(); expect(client).toBeDefined(); }); describe('Send tests', () => { beforeEach(() => { - client = BacktraceClient.builder({ - url: 'https://submit.backtrace.io/foo/bar/baz', - }) - .useRequestHandler(requestHandler) - .build(); + client = BacktraceClient.builder(defaultClientOptions).useRequestHandler(requestHandler).build(); }); it(`Should not throw an error when sending a message`, async () => { expect(async () => await client.send('test')).not.toThrow(); @@ -46,10 +46,7 @@ describe('Client tests', () => { const fileContent = fs.readFileSync(sampleFile, 'utf8'); it(`Should generate an attachment list based on the client options`, async () => { - client = BacktraceClient.builder({ - url: 'https://submit.backtrace.io/foo/bar/baz', - attachments: [sampleFile], - }) + client = BacktraceClient.builder({ ...defaultClientOptions, attachments: [sampleFile] }) .useRequestHandler(requestHandler) .build(); @@ -66,7 +63,7 @@ describe('Client tests', () => { it(`Should allow to setup bufer attachment`, async () => { const testedBuffer = Buffer.from('test'); client = BacktraceClient.builder({ - url: 'https://submit.backtrace.io/foo/bar/baz', + ...defaultClientOptions, attachments: [new BacktraceBufferAttachment('test', testedBuffer)], }) .useRequestHandler(requestHandler) @@ -79,12 +76,7 @@ describe('Client tests', () => { it(`Should allow to add more attachments`, async () => { const testedAttachment = new BacktraceFileAttachment(sampleFile); - client = BacktraceClient.builder({ - url: 'https://submit.backtrace.io/foo/bar/baz', - attachments: [], - }) - .useRequestHandler(requestHandler) - .build(); + client = BacktraceClient.builder(defaultClientOptions).useRequestHandler(requestHandler).build(); client.attachments.push(testedAttachment); expect(client.attachments).toBeDefined(); diff --git a/packages/sdk-core/src/BacktraceCoreClient.ts b/packages/sdk-core/src/BacktraceCoreClient.ts index 4a507b82..b203f5df 100644 --- a/packages/sdk-core/src/BacktraceCoreClient.ts +++ b/packages/sdk-core/src/BacktraceCoreClient.ts @@ -1,6 +1,10 @@ -import { BacktraceAttachment, BacktraceAttributeProvider, BacktraceStackTraceConverter } from '.'; +import { + BacktraceAttachment, + BacktraceAttributeProvider, + BacktraceSessionProvider, + BacktraceStackTraceConverter, +} from '.'; import { SdkOptions } from './builder/SdkOptions'; -import { IdGenerator } from './common/IdGenerator'; import { BacktraceConfiguration } from './model/configuration/BacktraceConfiguration'; import { AttributeType } from './model/data/BacktraceData'; import { BacktraceReportSubmission } from './model/http/BacktraceReportSubmission'; @@ -10,13 +14,16 @@ import { AttributeManager } from './modules/attribute/AttributeManager'; import { ClientAttributeProvider } from './modules/attribute/ClientAttributeProvider'; import { V8StackTraceConverter } from './modules/converter/V8StackTraceConverter'; import { BacktraceDataBuilder } from './modules/data/BacktraceDataBuilder'; +import { BacktraceMetrics } from './modules/metrics/BacktraceMetrics'; +import { MetricsBuilder } from './modules/metrics/MetricsBuilder'; +import { SingleSessionProvider } from './modules/metrics/SingleSessionProvider'; import { RateLimitWatcher } from './modules/rateLimiter/RateLimitWatcher'; export abstract class BacktraceCoreClient { /** * Current session id */ public get sessionId(): string { - return this._sessionId; + return this.sessionProvider.sessionId; } /** @@ -46,6 +53,10 @@ export abstract class BacktraceCoreClient { return this._attributeProvider.annotations; } + public get metrics(): BacktraceMetrics | undefined { + return this._metrics; + } + /** * Client cached attachments */ @@ -55,11 +66,7 @@ export abstract class BacktraceCoreClient { private readonly _reportSubmission: BacktraceReportSubmission; private readonly _rateLimitWatcher: RateLimitWatcher; private readonly _attributeProvider: AttributeManager; - - /** - * Current session Id - */ - private readonly _sessionId: string = IdGenerator.uuid(); + private readonly _metrics?: BacktraceMetrics; protected constructor( protected readonly options: BacktraceConfiguration, @@ -67,15 +74,25 @@ export abstract class BacktraceCoreClient { requestHandler: BacktraceRequestHandler, attributeProviders: BacktraceAttributeProvider[] = [], stackTraceConverter: BacktraceStackTraceConverter = new V8StackTraceConverter(), + private sessionProvider: BacktraceSessionProvider = new SingleSessionProvider(), ) { this._dataBuilder = new BacktraceDataBuilder(this._sdkOptions, stackTraceConverter); this._reportSubmission = new BacktraceReportSubmission(options, requestHandler); this._rateLimitWatcher = new RateLimitWatcher(options.rateLimit); this._attributeProvider = new AttributeManager([ - new ClientAttributeProvider(_sdkOptions.agentVersion, this._sessionId, options.userAttributes ?? {}), + new ClientAttributeProvider( + _sdkOptions.agentVersion, + sessionProvider.sessionId, + options.userAttributes ?? {}, + ), ...(attributeProviders ?? []), ]); this.attachments = options.attachments ?? []; + const metrics = new MetricsBuilder(options, sessionProvider, this._attributeProvider, requestHandler).build(); + if (metrics) { + this._metrics = metrics; + this._metrics.start(); + } } /** diff --git a/packages/sdk-core/src/common/DelayHelper.ts b/packages/sdk-core/src/common/DelayHelper.ts new file mode 100644 index 00000000..1686d790 --- /dev/null +++ b/packages/sdk-core/src/common/DelayHelper.ts @@ -0,0 +1,22 @@ +export class Delay { + /** + * Promise set timeout wrapper. + * @param timeout timeout in ms + * @param signal abort signal + */ + public static wait(timeout: number, signal?: AbortSignal) { + return new Promise((resolve) => { + function abortCallback() { + clearTimeout(intervalId); + resolve(); + } + + const intervalId = setTimeout(() => { + signal?.removeEventListener('abort', abortCallback); + resolve(); + }, timeout); + + signal?.addEventListener('abort', abortCallback); + }); + } +} diff --git a/packages/sdk-core/src/common/TimeHelper.ts b/packages/sdk-core/src/common/TimeHelper.ts index f3273f91..51eb3935 100644 --- a/packages/sdk-core/src/common/TimeHelper.ts +++ b/packages/sdk-core/src/common/TimeHelper.ts @@ -6,4 +6,8 @@ export class TimeHelper { public static toTimestampInSec(timestampMs: number): number { return Math.floor(timestampMs / 1000); } + + public static convertSecondsToMilliseconds(timeInSec: number): number { + return timeInSec * 1000; + } } diff --git a/packages/sdk-core/src/index.ts b/packages/sdk-core/src/index.ts index 6aacfb70..bd89b6e6 100644 --- a/packages/sdk-core/src/index.ts +++ b/packages/sdk-core/src/index.ts @@ -10,3 +10,4 @@ export * from './model/report/BacktraceErrorType'; export * from './model/report/BacktraceReport'; export * from './modules/attribute/BacktraceAttributeProvider'; export * from './modules/converter'; +export * from './modules/metrics/BacktraceSessionProvider'; diff --git a/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts b/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts index 09221d5f..5f1212e1 100644 --- a/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts +++ b/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts @@ -1,15 +1,27 @@ import { BacktraceAttachment } from '../attachment'; import { BacktraceDatabaseConfiguration } from './BacktraceDatabaseConfiguration'; -export interface BacktraceMetricsSupport { +export interface BacktraceMetricsOptions { + /** + * Metrics server hostname. By default the value is set to: https://events.backtrace.io. + */ metricsSubmissionUrl?: string; - enable: boolean; - ignoreSslCertificate?: boolean; /** - * Indicates how often crash free metrics are sent to Backtrace. + * Determinates if the metrics support is enabled. By default the value is set to true. + */ + enable?: boolean; + /** + * Indicates how often crash free metrics are sent to Backtrace. The interval is a value in ms. * By default, session events are sent on application startup/finish, and every 30 minutes while the game is running. + * If the value is set to 0. The auto send mode is disabled. In this situation the application needs to maintain send + * mode manually. */ autoSendInterval?: number; + + /** + * Indicates how many events the metrics storage can store before auto submission. + */ + size?: number; } export interface BacktraceConfiguration { @@ -50,7 +62,7 @@ export interface BacktraceConfiguration { /** * Metrics such as crash free users and crash free sessions */ - metrics?: BacktraceMetricsSupport; + metrics?: BacktraceMetricsOptions; /** * Offline database settings */ diff --git a/packages/sdk-core/src/modules/metrics/BacktraceMetrics.ts b/packages/sdk-core/src/modules/metrics/BacktraceMetrics.ts new file mode 100644 index 00000000..4d88cd03 --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/BacktraceMetrics.ts @@ -0,0 +1,127 @@ +import { TimeHelper } from '../../common/TimeHelper'; +import { BacktraceMetricsOptions } from '../../model/configuration/BacktraceConfiguration'; +import { AttributeType } from '../../model/data/BacktraceData'; +import { AttributeManager } from '../attribute/AttributeManager'; +import { ReportDataBuilder } from '../attribute/ReportDataBuilder'; +import { BacktraceSessionProvider } from './BacktraceSessionProvider'; +import { MetricsQueue } from './MetricsQueue'; +import { SummedEvent } from './model/SummedEvent'; +import { UniqueEvent } from './model/UniqueEvent'; + +export class BacktraceMetrics { + /** + * Returns current session id. + */ + public get sessionId() { + return this._sessionProvider.sessionId; + } + + /** + * Default metrics submission interval. The variable defines how often metrics will be sent to metrics system. + */ + public readonly DEFAULT_UPDATE_INTERVAL = TimeHelper.convertSecondsToMilliseconds(30 * 60); + public readonly DEFAULT_SERVER_URL = 'https://events.backtrace.io'; + + public readonly metricsHost: string = this._options.metricsSubmissionUrl ?? this.DEFAULT_SERVER_URL; + private readonly _updateInterval: number = this._options.autoSendInterval ?? this.DEFAULT_UPDATE_INTERVAL; + + private _updateIntervalId?: ReturnType; + + constructor( + private readonly _options: BacktraceMetricsOptions, + private readonly _sessionProvider: BacktraceSessionProvider, + private readonly _attributeManager: AttributeManager, + private readonly _summedEventsSubmissionQueue: MetricsQueue, + private readonly _uniqueEventsSubmissionQueue: MetricsQueue, + ) {} + + /** + * Starts metrics submission. + */ + public start() { + if (!this._sessionProvider.newSession) { + return; + } + + const { attributes } = this._attributeManager.get(); + this._summedEventsSubmissionQueue.add( + new SummedEvent('Application Launches', this.convertAttributes(attributes)), + ); + this.send(); + + if (this._updateInterval === 0) { + return; + } + this._updateIntervalId = setInterval(() => { + this.send(); + }, this._updateInterval); + } + + /** + * Returns total number of events in the submission queue. + */ + public count() { + return this._summedEventsSubmissionQueue?.total ?? 0 + this._uniqueEventsSubmissionQueue?.total ?? 0; + } + + /** + * Add summed event to next Backtrace Metrics request. + * @param metricName Summed event name. + * @param eventAttributes event attributes. + */ + public addSummedEvent(metricName: string, eventAttributes: Record = {}): boolean { + if (!metricName) { + return false; + } + const attributes = this.convertAttributes({ + ...this._attributeManager.get().attributes, + ...ReportDataBuilder.build(eventAttributes ?? {}).attributes, + }); + + this._summedEventsSubmissionQueue.add(new SummedEvent(metricName, attributes)); + + return true; + } + + /** + * Sends event to the metrics system. + */ + public send() { + if (!this._sessionProvider.shouldSend()) { + return false; + } + this.sendUniqueEvent(); + this._summedEventsSubmissionQueue.send(); + this._sessionProvider.afterMetricsSubmission(); + return true; + } + + /** + * Cleans up metrics interface. + */ + public close() { + if (this._updateIntervalId) { + clearInterval(this._updateIntervalId); + } + } + + private sendUniqueEvent() { + // always add the same unique event before send. + const { attributes } = this._attributeManager.get(); + this._uniqueEventsSubmissionQueue.add(new UniqueEvent(this.convertAttributes(attributes))); + this._uniqueEventsSubmissionQueue.send(); + } + + /** + * Event aggregators expecting to retrieve attributes in a string format. They also + * don't expect to retrieve null/undefined as attribute values. + */ + private convertAttributes(attributes: Record) { + return Object.keys(attributes) + .filter((n) => attributes[n] != null) + .reduce((acc, n) => { + acc[n] = attributes[n]?.toString(); + return acc; + }, {} as Record); + } +} diff --git a/packages/sdk-core/src/modules/metrics/BacktraceSessionProvider.ts b/packages/sdk-core/src/modules/metrics/BacktraceSessionProvider.ts new file mode 100644 index 00000000..518a979a --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/BacktraceSessionProvider.ts @@ -0,0 +1,20 @@ +export interface BacktraceSessionProvider { + /** + * Determinates if the session just started + */ + readonly newSession: boolean; + + /** + * Current session id + */ + readonly sessionId: string; + + /** + * Returns last submission timestamp. If 0 it means metrics weren't send + */ + get lastActive(): number; + + afterMetricsSubmission(): void; + + shouldSend(): boolean; +} diff --git a/packages/sdk-core/src/modules/metrics/MetricsBuilder.ts b/packages/sdk-core/src/modules/metrics/MetricsBuilder.ts new file mode 100644 index 00000000..a238b6f1 --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/MetricsBuilder.ts @@ -0,0 +1,137 @@ +import { TimeHelper } from '../../common/TimeHelper'; +import { BacktraceConfiguration, BacktraceMetricsOptions } from '../../model/configuration/BacktraceConfiguration'; +import { BacktraceRequestHandler } from '../../model/http'; +import { AttributeManager } from '../attribute/AttributeManager'; +import { BacktraceMetrics } from './BacktraceMetrics'; +import { BacktraceSessionProvider } from './BacktraceSessionProvider'; +import { MetricsQueue } from './MetricsQueue'; +import { MetricsSubmissionQueue } from './MetricsSubmissionQueue'; +import { MetricsUrlInformation } from './MetricsUrlInformation'; +import { SummedEvent } from './model/SummedEvent'; +import { UniqueEvent } from './model/UniqueEvent'; + +interface ApplicationInfo { + application: string; + applicationVersion: string; +} +export class MetricsBuilder { + /** + * Default metrics submission interval. The variable defines how often metrics will be sent to metrics system + * By default 30 mins. + */ + public readonly DEFAULT_UPDATE_INTERVAL = TimeHelper.convertSecondsToMilliseconds(30 * 60); + + private readonly APPLICATION_VERSION_ATTRIBUTE = 'application.version'; + private readonly APPLICATION_ATTRIBUTE = 'application'; + constructor( + private readonly _options: BacktraceConfiguration, + private readonly _sessionProvider: BacktraceSessionProvider, + private readonly _attributeManager: AttributeManager, + private readonly _requestHandler: BacktraceRequestHandler, + ) {} + + public build( + uniqueEventsSubmissionQueue?: MetricsQueue, + summedEventsSubmissionQueue?: MetricsQueue, + ): BacktraceMetrics | undefined { + const metricsOptions = { + ...this.optionsWithDefaults(), + ...(this._options.metrics ?? {}), + }; + if (!metricsOptions.enable) { + return undefined; + } + const applicationInfo = this.verifyAttributeSetup(); + if (!applicationInfo) { + return undefined; + } + + uniqueEventsSubmissionQueue = + uniqueEventsSubmissionQueue ?? + this.createUniqueEventSubmissionQueue(metricsOptions.metricsSubmissionUrl as string, applicationInfo); + if (!uniqueEventsSubmissionQueue) { + return undefined; + } + + summedEventsSubmissionQueue = + summedEventsSubmissionQueue ?? + this.createSummedEventSubmissionQueue(metricsOptions.metricsSubmissionUrl as string, applicationInfo); + if (!summedEventsSubmissionQueue) { + return undefined; + } + + return new BacktraceMetrics( + metricsOptions, + this._sessionProvider, + this._attributeManager, + summedEventsSubmissionQueue, + uniqueEventsSubmissionQueue, + ); + } + + private verifyAttributeSetup(): ApplicationInfo | undefined { + const { attributes } = this._attributeManager.get(); + const application = attributes[this.APPLICATION_ATTRIBUTE] as string; + const applicationVersion = attributes[this.APPLICATION_VERSION_ATTRIBUTE] as string; + if (!application || !applicationVersion) { + return undefined; + } + + return { application, applicationVersion }; + } + + private createUniqueEventSubmissionQueue(metricsHost: string, applicationInfo: ApplicationInfo) { + const uniqueEventsSubmissionUrl = MetricsUrlInformation.generateUniqueEventsUrl( + metricsHost, + this._options.url, + this._options.token, + ); + + if (!uniqueEventsSubmissionUrl) { + return undefined; + } + + return new MetricsSubmissionQueue( + uniqueEventsSubmissionUrl, + 'unique_events', + this._requestHandler, + { + [this.APPLICATION_ATTRIBUTE]: applicationInfo.application, + appversion: applicationInfo.applicationVersion, + }, + this._options?.metrics?.size, + ); + } + + private createSummedEventSubmissionQueue(metricsHost: string, applicationInfo: ApplicationInfo) { + const summedEventsSubmissionUrl = MetricsUrlInformation.generateSummedEventsUrl( + metricsHost, + this._options.url, + this._options.token, + ); + + if (!summedEventsSubmissionUrl) { + return undefined; + } + + return new MetricsSubmissionQueue( + summedEventsSubmissionUrl, + 'summed_events', + this._requestHandler, + { + [this.APPLICATION_ATTRIBUTE]: applicationInfo.application, + appversion: applicationInfo.applicationVersion, + }, + this._options?.metrics?.size, + ); + } + + private optionsWithDefaults(): BacktraceMetricsOptions { + return { + enable: true, + autoSendInterval: this.DEFAULT_UPDATE_INTERVAL, + metricsSubmissionUrl: 'https://events.backtrace.io', + size: 50, + }; + } +} diff --git a/packages/sdk-core/src/modules/metrics/MetricsQueue.ts b/packages/sdk-core/src/modules/metrics/MetricsQueue.ts new file mode 100644 index 00000000..6facf220 --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/MetricsQueue.ts @@ -0,0 +1,7 @@ +export interface MetricsQueue { + total: number; + submissionUrl: string; + maximumEvents: number; + add(event: T): void; + send(): Promise; +} diff --git a/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts b/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts new file mode 100644 index 00000000..d7d3d128 --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts @@ -0,0 +1,82 @@ +import { Delay } from '../../common/DelayHelper'; +import { TimeHelper } from '../../common/TimeHelper'; +import { BacktraceRequestHandler } from '../../model/http'; +import { MetricsQueue } from './MetricsQueue'; +import { MetricsEvent } from './model/MetricsEvent'; + +export class MetricsSubmissionQueue implements MetricsQueue { + public get total() { + return this._events.length; + } + + public get submissionUrl() { + return this._submissionUrl; + } + + public readonly TIMEOUT_BETWEEN_REQUESTS = TimeHelper.convertSecondsToMilliseconds(10); + + private _events: T[] = []; + private _numberOfDroppedRequests = 0; + + private readonly MAXIMUM_NUMBER_OF_ATTEMPTS = 3; + + constructor( + private readonly _submissionUrl: string, + private readonly _eventName: string, + private readonly _requestHandler: BacktraceRequestHandler, + private readonly _metricMetadata: Record, + public readonly maximumEvents: number = 50, + ) {} + + public add(event: T) { + this._events.push(event); + if (this.reachedLimit()) { + this.send(); + } + } + + public async send() { + const eventsToProcess = this._events.splice(0); + return await this.submitMetricsToServer(eventsToProcess); + } + + private async submitMetricsToServer(events: T[], attempts = 0) { + if (attempts >= this.MAXIMUM_NUMBER_OF_ATTEMPTS) { + this.returnEventsIfPossible(events); + return; + } + + const response = await this._requestHandler.post( + this._submissionUrl, + JSON.stringify({ + ...this._metricMetadata, + [this._eventName]: events, + metadata: { + dropped_events: this._numberOfDroppedRequests, + }, + }), + ); + if (response.status !== 'Ok') { + this._numberOfDroppedRequests++; + attempts += 1; + await Delay.wait(2 ** attempts * this.TIMEOUT_BETWEEN_REQUESTS); + await this.submitMetricsToServer(events, attempts); + return; + } + + this._numberOfDroppedRequests = 0; + } + + private returnEventsIfPossible(events: T[]) { + if (this.maximumEvents < this._events.length + events.length) { + return; + } + + // push events to the beginning of the queue + this._events = events.concat(this._events); + } + + private reachedLimit() { + return this.maximumEvents === this._events.length && this.maximumEvents !== 0; + } +} diff --git a/packages/sdk-core/src/modules/metrics/SingleSessionProvider.ts b/packages/sdk-core/src/modules/metrics/SingleSessionProvider.ts new file mode 100644 index 00000000..87b6abf1 --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/SingleSessionProvider.ts @@ -0,0 +1,24 @@ +import { IdGenerator } from '../../common/IdGenerator'; +import { TimeHelper } from '../../common/TimeHelper'; +import { BacktraceSessionProvider } from './BacktraceSessionProvider'; + +export class SingleSessionProvider implements BacktraceSessionProvider { + public readonly newSession: boolean = true; + public readonly sessionId: string = IdGenerator.uuid(); + private _lastActive = 0; + + public get lastActive() { + return this._lastActive; + } + + public afterMetricsSubmission(): void { + this._lastActive = TimeHelper.now(); + } + /** + * Allow to alway send metrics - in the single session there is no reason + * to skip sending metrics. + */ + public shouldSend(): boolean { + return true; + } +} diff --git a/packages/sdk-core/src/modules/metrics/model/MetricsEvent.ts b/packages/sdk-core/src/modules/metrics/model/MetricsEvent.ts new file mode 100644 index 00000000..9235ec05 --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/model/MetricsEvent.ts @@ -0,0 +1,20 @@ +import { IdGenerator } from '../../../common/IdGenerator'; +import { TimeHelper } from '../../../common/TimeHelper'; +import { AttributeType } from '../../../model/data/BacktraceData'; + +export class MetricsEvent { + public readonly id = IdGenerator.uuid(); + constructor( + public readonly metricGroupName: string, + public readonly metricGroupValue: string | string[], + public readonly attributes: Record = {}, + ) {} + + public toJSON() { + return { + timestamp: TimeHelper.toTimestampInSec(TimeHelper.now()), + attributes: this.attributes, + [this.metricGroupName]: this.metricGroupValue, + }; + } +} diff --git a/packages/sdk-core/src/modules/metrics/model/SummedEvent.ts b/packages/sdk-core/src/modules/metrics/model/SummedEvent.ts new file mode 100644 index 00000000..4356675f --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/model/SummedEvent.ts @@ -0,0 +1,8 @@ +import { AttributeType } from '../../../model/data/BacktraceData'; +import { MetricsEvent } from './MetricsEvent'; + +export class SummedEvent extends MetricsEvent { + constructor(metricGroupName: string, attributes: Record = {}) { + super('metric_group', metricGroupName, attributes); + } +} diff --git a/packages/sdk-core/src/modules/metrics/model/UniqueEvent.ts b/packages/sdk-core/src/modules/metrics/model/UniqueEvent.ts new file mode 100644 index 00000000..4bcc0e22 --- /dev/null +++ b/packages/sdk-core/src/modules/metrics/model/UniqueEvent.ts @@ -0,0 +1,8 @@ +import { AttributeType } from '../../../model/data/BacktraceData'; +import { MetricsEvent } from './MetricsEvent'; + +export class UniqueEvent extends MetricsEvent { + constructor(attributes: Record) { + super('unique', ['guid'], attributes); + } +} diff --git a/packages/sdk-core/tests/metrics/metricSetupTests.spec.ts b/packages/sdk-core/tests/metrics/metricSetupTests.spec.ts new file mode 100644 index 00000000..833ad8fc --- /dev/null +++ b/packages/sdk-core/tests/metrics/metricSetupTests.spec.ts @@ -0,0 +1,127 @@ +import { AttributeManager } from '../../src/modules/attribute/AttributeManager'; +import { BacktraceSingleSessionProvider } from '../../src/modules/metrics/BacktraceSingleSessionProvider'; +import { MetricsBuilder } from '../../src/modules/metrics/MetricsBuilder'; +import { APPLICATION, APPLICATION_VERSION, TEST_SUBMISSION_URL } from '../mocks/BacktraceTestClient'; +import { testHttpClient } from '../mocks/testHttpClient'; + +describe('Metric setup', () => { + let attributeManager: AttributeManager; + + beforeEach(() => { + attributeManager = new AttributeManager([]); + }); + + describe('Enabled metrics', () => { + it('Should successfuly build metrics client', () => { + attributeManager.add({ + application: APPLICATION, + ['application.version']: APPLICATION_VERSION, + }); + + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + }, + }, + new BacktraceSingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(); + + expect(metrics).toBeDefined(); + }); + + it('Should successfuly send metrics', () => { + attributeManager.add({ + application: APPLICATION, + ['application.version']: APPLICATION_VERSION, + }); + + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + }, + }, + new BacktraceSingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(); + + if (!metrics) { + return fail('Metrics is not defined'); + } + expect(metrics.send()).toBeTruthy(); + }); + }); + describe('Disabled metrics', () => { + it(`Shouldn't build a client with invalid url`, () => { + const metrics = new MetricsBuilder( + { + url: 'https://definitely-different.submission.url', + metrics: { + autoSendInterval: 0, + }, + }, + new BacktraceSingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(); + + expect(metrics).toBeUndefined(); + }); + + it(`Shouldn't build a client without application name`, () => { + attributeManager.add({ + application: undefined, + ['application.version']: APPLICATION_VERSION, + }); + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + }, + new BacktraceSingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(); + + expect(metrics).toBeUndefined(); + }); + + it(`Shouldn't build a client without application version`, () => { + attributeManager.add({ + application: APPLICATION, + ['application.version']: undefined, + }); + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + }, + new BacktraceSingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(); + + expect(metrics).toBeUndefined(); + }); + + it('Should not build the metric client if metrics are disabled', () => { + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + enable: false, + }, + }, + new BacktraceSingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(); + + expect(metrics).toBeUndefined(); + }); + }); +}); diff --git a/packages/sdk-core/tests/metrics/mocks/mockSubmissionQueue.ts b/packages/sdk-core/tests/metrics/mocks/mockSubmissionQueue.ts new file mode 100644 index 00000000..9000a8ff --- /dev/null +++ b/packages/sdk-core/tests/metrics/mocks/mockSubmissionQueue.ts @@ -0,0 +1,14 @@ +import { MetricsQueue } from '../../../src/modules/metrics/MetricsQueue'; +import { MetricsEvent } from '../../../src/modules/metrics/model/MetricsEvent'; + +export const mockSubmissionQueue: MetricsQueue = { + total: 0, + submissionUrl: 'fake-http-url', + maximumEvents: 0, + add: () => { + return; + }, + send: () => { + return Promise.resolve(); + }, +}; diff --git a/packages/sdk-core/tests/metrics/summedEventTests.spec.ts b/packages/sdk-core/tests/metrics/summedEventTests.spec.ts new file mode 100644 index 00000000..6473f2cd --- /dev/null +++ b/packages/sdk-core/tests/metrics/summedEventTests.spec.ts @@ -0,0 +1,193 @@ +import { TimeHelper } from '../../src/common/TimeHelper'; +import { AttributeType } from '../../src/model/data/BacktraceData'; +import { AttributeManager } from '../../src/modules/attribute/AttributeManager'; +import { MetricsBuilder } from '../../src/modules/metrics/MetricsBuilder'; +import { MetricsUrlInformation } from '../../src/modules/metrics/MetricsUrlInformation'; +import { SummedEvent } from '../../src/modules/metrics/model/SummedEvent'; +import { SingleSessionProvider } from '../../src/modules/metrics/SingleSessionProvider'; +import { APPLICATION, APPLICATION_VERSION, TEST_SUBMISSION_URL } from '../mocks/BacktraceTestClient'; +import { testHttpClient } from '../mocks/testHttpClient'; +import { mockSubmissionQueue } from './mocks/mockSubmissionQueue'; + +describe('Summed events tests', () => { + let timestamp: number; + const attributeManager = new AttributeManager([]); + + beforeEach(() => { + attributeManager.add({ + application: APPLICATION, + ['application.version']: APPLICATION_VERSION, + }); + timestamp = TimeHelper.now(); + jest.spyOn(TimeHelper, 'now').mockImplementation(() => { + return timestamp; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should send summed event on the metrics start', () => { + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(mockSubmissionQueue); + + if (!metrics) { + fail('Metrics are not defined'); + } + const summedEventsSubmissionUrl = MetricsUrlInformation.generateSummedEventsUrl( + metrics.metricsHost, + TEST_SUBMISSION_URL, + ); + + const expectedJson = { + application: APPLICATION, + appversion: APPLICATION_VERSION, + summed_events: [new SummedEvent('Application Launches', attributeManager.attributes)], + metadata: { + dropped_events: 0, + }, + }; + + metrics.start(); + + expect(testHttpClient.post).toBeCalledWith(summedEventsSubmissionUrl, JSON.stringify(expectedJson)); + }); + + it('Should send summed event to overriden submission URL', () => { + const expectedBaseUrl = 'https://test-metrics-submission-url.com'; + + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + metricsSubmissionUrl: expectedBaseUrl, + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(mockSubmissionQueue); + + if (!metrics) { + fail('Metrics are not defined'); + } + const summedEventsSubmissionUrl = MetricsUrlInformation.generateSummedEventsUrl( + metrics.metricsHost, + TEST_SUBMISSION_URL, + ); + + const expectedJson = { + application: APPLICATION, + appversion: APPLICATION_VERSION, + summed_events: [new SummedEvent('Application Launches', attributeManager.attributes)], + metadata: { + dropped_events: 0, + }, + }; + + metrics.start(); + + expect(testHttpClient.post).toBeCalledWith(summedEventsSubmissionUrl, JSON.stringify(expectedJson)); + }); + + it('Should send summed event with custom attributes to the server', () => { + const customAttributes: Record = { + 'custom-attribute': 'custom-attribute', + 'second-attribute': 'false', + }; + attributeManager.add(customAttributes); + + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(mockSubmissionQueue); + + if (!metrics) { + fail('Metrics are not defined'); + } + const expectedJson = { + application: APPLICATION, + appversion: APPLICATION_VERSION, + summed_events: [new SummedEvent('Application Launches', attributeManager.attributes)], + metadata: { + dropped_events: 0, + }, + }; + + metrics.start(); + + expect(attributeManager.attributes).toMatchObject(customAttributes); + expect(testHttpClient.post).toBeCalledWith(expect.anything(), JSON.stringify(expectedJson)); + }); + + it('Should add summed event to the submission queue', () => { + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(mockSubmissionQueue); + + if (!metrics) { + fail('Metrics are not defined'); + } + metrics.start(); + const addResult = metrics.addSummedEvent('test-metric'); + + expect(addResult).toBeTruthy(); + expect(testHttpClient.post).toBeCalledTimes(1); + expect(metrics.count()).toEqual(1); + }); + + it('Should send summed events to server when reached the limit', () => { + const maximumNumberOfEvents = 3; + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + size: maximumNumberOfEvents, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(mockSubmissionQueue); + + if (!metrics) { + fail('Metrics are not defined'); + } + + metrics.start(); + + for (let index = 0; index < maximumNumberOfEvents; index++) { + const addResult = metrics.addSummedEvent('test-metric'); + expect(addResult).toBeTruthy(); + } + + expect(testHttpClient.post).toBeCalledTimes(2); + }); +}); diff --git a/packages/sdk-core/tests/metrics/uniqueEventTests.spec.ts b/packages/sdk-core/tests/metrics/uniqueEventTests.spec.ts new file mode 100644 index 00000000..73af1bc6 --- /dev/null +++ b/packages/sdk-core/tests/metrics/uniqueEventTests.spec.ts @@ -0,0 +1,172 @@ +import { fail } from 'assert'; +import { TimeHelper } from '../../src/common/TimeHelper'; +import { AttributeType } from '../../src/model/data/BacktraceData'; +import { AttributeManager } from '../../src/modules/attribute/AttributeManager'; +import { MetricsBuilder } from '../../src/modules/metrics/MetricsBuilder'; +import { MetricsUrlInformation } from '../../src/modules/metrics/MetricsUrlInformation'; +import { UniqueEvent } from '../../src/modules/metrics/model/UniqueEvent'; +import { SingleSessionProvider } from '../../src/modules/metrics/SingleSessionProvider'; +import { APPLICATION, APPLICATION_VERSION, TEST_SUBMISSION_URL } from '../mocks/BacktraceTestClient'; +import { testHttpClient } from '../mocks/testHttpClient'; +import { mockSubmissionQueue } from './mocks/mockSubmissionQueue'; + +describe('Unique events tests', () => { + let timestamp: number; + const attributeManager = new AttributeManager([]); + + beforeEach(() => { + attributeManager.add({ + application: APPLICATION, + ['application.version']: APPLICATION_VERSION, + }); + timestamp = TimeHelper.now(); + jest.spyOn(TimeHelper, 'now').mockImplementation(() => { + return timestamp; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should send unique event on the metrics start', () => { + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(undefined, mockSubmissionQueue); + + if (!metrics) { + fail('Metrics are not defined'); + } + const uniqueEventsSubmissionUrl = MetricsUrlInformation.generateUniqueEventsUrl( + metrics.metricsHost, + TEST_SUBMISSION_URL, + ); + + const expectedJson = { + application: APPLICATION, + appversion: APPLICATION_VERSION, + unique_events: [new UniqueEvent(attributeManager.attributes)], + metadata: { + dropped_events: 0, + }, + }; + + metrics.start(); + + expect(testHttpClient.post).toBeCalledWith(uniqueEventsSubmissionUrl, JSON.stringify(expectedJson)); + }); + + it('Should send unique event to overriden submission URL', () => { + const expectedBaseUrl = 'https://test-metrics-submission-url.com'; + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + metricsSubmissionUrl: expectedBaseUrl, + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(undefined, mockSubmissionQueue); + + if (!metrics) { + fail('Metrics are not defined'); + } + + const uniqueEventsSubmissionUrl = MetricsUrlInformation.generateUniqueEventsUrl( + expectedBaseUrl, + TEST_SUBMISSION_URL, + ); + + metrics.start(); + + expect(testHttpClient.post).toBeCalledWith(uniqueEventsSubmissionUrl, expect.anything()); + }); + + it(`Shouldn't build a client with invalid url`, () => { + const metrics = new MetricsBuilder( + { + url: 'https://definitely-different.submission.url', + metrics: { + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(); + + expect(metrics).toBeUndefined(); + }); + + it(`Shouldn't build a client without application name/version`, () => { + attributeManager.add({ + application: undefined, + ['application.version']: undefined, + }); + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(); + + expect(metrics).toBeUndefined(); + }); + + it('Should send unique event with custom attributes to the server', () => { + const customAttributes: Record = { + 'custom-attribute': 'custom-attribute', + 'second-attribute': 'false', + }; + attributeManager.add(customAttributes); + const metrics = new MetricsBuilder( + { + url: TEST_SUBMISSION_URL, + metrics: { + autoSendInterval: 0, + }, + }, + new SingleSessionProvider(), + attributeManager, + testHttpClient, + ).build(undefined, mockSubmissionQueue); + + if (!metrics) { + fail('Metrics are not defined'); + } + const uniqueEventsSubmissionUrl = MetricsUrlInformation.generateUniqueEventsUrl( + metrics.metricsHost, + TEST_SUBMISSION_URL, + ); + + const expectedJson = { + application: APPLICATION, + appversion: APPLICATION_VERSION, + unique_events: [new UniqueEvent(attributeManager.attributes)], + metadata: { + dropped_events: 0, + }, + }; + + metrics.start(); + + expect(attributeManager.attributes).toMatchObject(customAttributes); + expect(testHttpClient.post).toBeCalledWith(uniqueEventsSubmissionUrl, JSON.stringify(expectedJson)); + }); +}); diff --git a/packages/sdk-core/tests/mocks/BacktraceTestClient.ts b/packages/sdk-core/tests/mocks/BacktraceTestClient.ts index e58e7b20..27c47875 100644 --- a/packages/sdk-core/tests/mocks/BacktraceTestClient.ts +++ b/packages/sdk-core/tests/mocks/BacktraceTestClient.ts @@ -2,11 +2,14 @@ import { BacktraceAttachment, BacktraceAttributeProvider, BacktraceCoreClient, + BacktraceReportSubmissionResult, BacktraceRequestHandler, } from '../../src'; export const TOKEN = '590d39eb154cff1d30f2b689f9a928bb592b25e7e7c10192fe208485ea68d91c'; export const UNIVERSE_NAME = 'test'; export const TEST_SUBMISSION_URL = `https://${UNIVERSE_NAME}.sp.backtrace.io:6098/post?format=json&token=${TOKEN}`; +export const APPLICATION = 'test-app'; +export const APPLICATION_VERSION = '5.4.3'; export class BacktraceTestClient extends BacktraceCoreClient { public readonly requestHandler: BacktraceRequestHandler; constructor( @@ -19,6 +22,9 @@ export class BacktraceTestClient extends BacktraceCoreClient { url: TEST_SUBMISSION_URL, token: TOKEN, attachments, + metrics: { + enable: false, + }, }, { agent: 'test', @@ -36,9 +42,18 @@ export class BacktraceTestClient extends BacktraceCoreClient { attributeProviders: BacktraceAttributeProvider[] = [], attachments: BacktraceAttachment[] = [], ) { + attributeProviders.push({ + type: 'scoped', + get() { + return { + application: APPLICATION, + ['application.version']: APPLICATION_VERSION, + }; + }, + }); return new BacktraceTestClient( { - post: jest.fn().mockResolvedValue(Promise.resolve()), + post: jest.fn().mockResolvedValue(Promise.resolve(BacktraceReportSubmissionResult.Ok('Ok'))), postError: jest.fn().mockResolvedValue(Promise.resolve()), }, attributeProviders, diff --git a/packages/sdk-core/tests/mocks/testHttpClient.ts b/packages/sdk-core/tests/mocks/testHttpClient.ts new file mode 100644 index 00000000..ce2610d1 --- /dev/null +++ b/packages/sdk-core/tests/mocks/testHttpClient.ts @@ -0,0 +1,6 @@ +import { BacktraceReportSubmissionResult, BacktraceRequestHandler } from '../../src'; + +export const testHttpClient: BacktraceRequestHandler = { + post: jest.fn().mockResolvedValue(Promise.resolve(BacktraceReportSubmissionResult.Ok('Ok'))), + postError: jest.fn().mockResolvedValue(Promise.resolve()), +}; From 8c92345ebb7e71b7b99e988214ba94d3d0728536 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Wed, 28 Jun 2023 00:49:42 +0200 Subject: [PATCH 05/13] Nullable token --- .../sdk-core/src/modules/metrics/MetricsUrlInformation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts b/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts index 489f2ed3..b8903742 100644 --- a/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts +++ b/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts @@ -4,7 +4,7 @@ export class MetricsUrlInformation { public static generateSummedEventsUrl( hostname: string, submissionUrl: string, - credentialsToken?: string, + credentialsToken: string | null, ): string | undefined { const submissionInformation = this.findSubmissionInformation(submissionUrl, credentialsToken); if (!submissionInformation) { @@ -21,7 +21,7 @@ export class MetricsUrlInformation { public static generateUniqueEventsUrl( hostname: string, submissionUrl: string, - credentialsToken?: string, + credentialsToken: string | null, ): string | undefined { const submissionInformation = this.findSubmissionInformation(submissionUrl, credentialsToken); if (!submissionInformation) { @@ -47,7 +47,7 @@ export class MetricsUrlInformation { private static findSubmissionInformation( submissionUrl: string, - token?: string, + token: string | null, ): { universe: string; token: string } | undefined { const universe = SubmissionUrlInformation.findUniverse(submissionUrl); if (!universe) { From 0488a4f9051948f53fc1e72365c9f62dda82557c Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Wed, 28 Jun 2023 01:02:40 +0200 Subject: [PATCH 06/13] Accept null | undefined --- .../sdk-core/src/modules/metrics/MetricsUrlInformation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts b/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts index b8903742..39a47578 100644 --- a/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts +++ b/packages/sdk-core/src/modules/metrics/MetricsUrlInformation.ts @@ -4,7 +4,7 @@ export class MetricsUrlInformation { public static generateSummedEventsUrl( hostname: string, submissionUrl: string, - credentialsToken: string | null, + credentialsToken?: string | null, ): string | undefined { const submissionInformation = this.findSubmissionInformation(submissionUrl, credentialsToken); if (!submissionInformation) { @@ -21,7 +21,7 @@ export class MetricsUrlInformation { public static generateUniqueEventsUrl( hostname: string, submissionUrl: string, - credentialsToken: string | null, + credentialsToken?: string | null, ): string | undefined { const submissionInformation = this.findSubmissionInformation(submissionUrl, credentialsToken); if (!submissionInformation) { @@ -47,7 +47,7 @@ export class MetricsUrlInformation { private static findSubmissionInformation( submissionUrl: string, - token: string | null, + token?: string | null, ): { universe: string; token: string } | undefined { const universe = SubmissionUrlInformation.findUniverse(submissionUrl); if (!universe) { From 04ed4ee581714cc8b02d6e2f5154331b7fe2bbea Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Wed, 28 Jun 2023 01:05:49 +0200 Subject: [PATCH 07/13] Correct import --- .../tests/metrics/metricSetupTests.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/sdk-core/tests/metrics/metricSetupTests.spec.ts b/packages/sdk-core/tests/metrics/metricSetupTests.spec.ts index 833ad8fc..7d964a65 100644 --- a/packages/sdk-core/tests/metrics/metricSetupTests.spec.ts +++ b/packages/sdk-core/tests/metrics/metricSetupTests.spec.ts @@ -1,6 +1,6 @@ import { AttributeManager } from '../../src/modules/attribute/AttributeManager'; -import { BacktraceSingleSessionProvider } from '../../src/modules/metrics/BacktraceSingleSessionProvider'; import { MetricsBuilder } from '../../src/modules/metrics/MetricsBuilder'; +import { SingleSessionProvider } from '../../src/modules/metrics/SingleSessionProvider'; import { APPLICATION, APPLICATION_VERSION, TEST_SUBMISSION_URL } from '../mocks/BacktraceTestClient'; import { testHttpClient } from '../mocks/testHttpClient'; @@ -25,7 +25,7 @@ describe('Metric setup', () => { autoSendInterval: 0, }, }, - new BacktraceSingleSessionProvider(), + new SingleSessionProvider(), attributeManager, testHttpClient, ).build(); @@ -46,7 +46,7 @@ describe('Metric setup', () => { autoSendInterval: 0, }, }, - new BacktraceSingleSessionProvider(), + new SingleSessionProvider(), attributeManager, testHttpClient, ).build(); @@ -66,7 +66,7 @@ describe('Metric setup', () => { autoSendInterval: 0, }, }, - new BacktraceSingleSessionProvider(), + new SingleSessionProvider(), attributeManager, testHttpClient, ).build(); @@ -83,7 +83,7 @@ describe('Metric setup', () => { { url: TEST_SUBMISSION_URL, }, - new BacktraceSingleSessionProvider(), + new SingleSessionProvider(), attributeManager, testHttpClient, ).build(); @@ -100,7 +100,7 @@ describe('Metric setup', () => { { url: TEST_SUBMISSION_URL, }, - new BacktraceSingleSessionProvider(), + new SingleSessionProvider(), attributeManager, testHttpClient, ).build(); @@ -116,7 +116,7 @@ describe('Metric setup', () => { enable: false, }, }, - new BacktraceSingleSessionProvider(), + new SingleSessionProvider(), attributeManager, testHttpClient, ).build(); From e960bd39624a8b950f12b9b73e0ccd4d8a8a2d91 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Wed, 28 Jun 2023 12:35:46 +0200 Subject: [PATCH 08/13] Determines typo --- packages/browser/src/BacktraceBrowserSessionProvider.ts | 2 +- .../sdk-core/src/model/configuration/BacktraceConfiguration.ts | 2 +- .../sdk-core/src/modules/metrics/BacktraceSessionProvider.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/BacktraceBrowserSessionProvider.ts b/packages/browser/src/BacktraceBrowserSessionProvider.ts index 3152fd72..99616a20 100644 --- a/packages/browser/src/BacktraceBrowserSessionProvider.ts +++ b/packages/browser/src/BacktraceBrowserSessionProvider.ts @@ -17,7 +17,7 @@ export class BacktraceBrowserSessionProvider implements BacktraceSessionProvider return this._lastActive; } /** - * Determinates if the current session is new. + * Determines if the current session is new. */ public readonly newSession: boolean = true; /** diff --git a/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts b/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts index 5f1212e1..bd957c71 100644 --- a/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts +++ b/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts @@ -7,7 +7,7 @@ export interface BacktraceMetricsOptions { */ metricsSubmissionUrl?: string; /** - * Determinates if the metrics support is enabled. By default the value is set to true. + * Determines if the metrics support is enabled. By default the value is set to true. */ enable?: boolean; /** diff --git a/packages/sdk-core/src/modules/metrics/BacktraceSessionProvider.ts b/packages/sdk-core/src/modules/metrics/BacktraceSessionProvider.ts index 518a979a..abbc5a0a 100644 --- a/packages/sdk-core/src/modules/metrics/BacktraceSessionProvider.ts +++ b/packages/sdk-core/src/modules/metrics/BacktraceSessionProvider.ts @@ -1,6 +1,6 @@ export interface BacktraceSessionProvider { /** - * Determinates if the session just started + * Determines if the session just started */ readonly newSession: boolean; From 90aa99c3c6d204f50d678f25a4ebce662464a35d Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Thu, 29 Jun 2023 15:15:14 +0200 Subject: [PATCH 09/13] Code review adjustements --- .../src/BacktraceBrowserSessionProvider.ts | 16 ++++++---------- packages/sdk-core/src/BacktraceCoreClient.ts | 8 ++++---- .../configuration/BacktraceConfiguration.ts | 4 ++-- .../src/modules/metrics/BacktraceMetrics.ts | 5 +---- .../sdk-core/src/modules/metrics/MetricsQueue.ts | 6 +++--- .../modules/metrics/MetricsSubmissionQueue.ts | 4 ++-- 6 files changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/browser/src/BacktraceBrowserSessionProvider.ts b/packages/browser/src/BacktraceBrowserSessionProvider.ts index 99616a20..e9e078c7 100644 --- a/packages/browser/src/BacktraceBrowserSessionProvider.ts +++ b/packages/browser/src/BacktraceBrowserSessionProvider.ts @@ -10,19 +10,12 @@ export class BacktraceBrowserSessionProvider implements BacktraceSessionProvider private readonly SESSION_LAST_ACTIVE = 'backtrace-last-active'; private readonly SESSION_GUID = 'backtrace-guid'; - /** - * Session last active timestamp - */ get lastActive(): number { return this._lastActive; } - /** - * Determines if the current session is new. - */ + public readonly newSession: boolean = true; - /** - * Current session Id - */ + public readonly sessionId: string = IdGenerator.uuid(); private _lastActive = 0; @@ -48,6 +41,9 @@ export class BacktraceBrowserSessionProvider implements BacktraceSessionProvider } public shouldSend(): boolean { + // if the document is hidden, we shouldn't send metrics, because the open document + // is the one who is being used by the user. This condition makes sure two or more web + // browser tabs of the same app won't report the same metrics or false positive metrics. return document.hidden === false; } @@ -65,7 +61,7 @@ export class BacktraceBrowserSessionProvider implements BacktraceSessionProvider return lastActive; } - private updateLastActiveTimestamp() { + public updateLastActiveTimestamp() { this._lastActive = TimeHelper.now(); localStorage.setItem(this.SESSION_LAST_ACTIVE, this._lastActive.toString(10)); } diff --git a/packages/sdk-core/src/BacktraceCoreClient.ts b/packages/sdk-core/src/BacktraceCoreClient.ts index b203f5df..763cc57b 100644 --- a/packages/sdk-core/src/BacktraceCoreClient.ts +++ b/packages/sdk-core/src/BacktraceCoreClient.ts @@ -23,7 +23,7 @@ export abstract class BacktraceCoreClient { * Current session id */ public get sessionId(): string { - return this.sessionProvider.sessionId; + return this._sessionProvider.sessionId; } /** @@ -74,7 +74,7 @@ export abstract class BacktraceCoreClient { requestHandler: BacktraceRequestHandler, attributeProviders: BacktraceAttributeProvider[] = [], stackTraceConverter: BacktraceStackTraceConverter = new V8StackTraceConverter(), - private sessionProvider: BacktraceSessionProvider = new SingleSessionProvider(), + private readonly _sessionProvider: BacktraceSessionProvider = new SingleSessionProvider(), ) { this._dataBuilder = new BacktraceDataBuilder(this._sdkOptions, stackTraceConverter); this._reportSubmission = new BacktraceReportSubmission(options, requestHandler); @@ -82,13 +82,13 @@ export abstract class BacktraceCoreClient { this._attributeProvider = new AttributeManager([ new ClientAttributeProvider( _sdkOptions.agentVersion, - sessionProvider.sessionId, + _sessionProvider.sessionId, options.userAttributes ?? {}, ), ...(attributeProviders ?? []), ]); this.attachments = options.attachments ?? []; - const metrics = new MetricsBuilder(options, sessionProvider, this._attributeProvider, requestHandler).build(); + const metrics = new MetricsBuilder(options, _sessionProvider, this._attributeProvider, requestHandler).build(); if (metrics) { this._metrics = metrics; this._metrics.start(); diff --git a/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts b/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts index bd957c71..79972ac5 100644 --- a/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts +++ b/packages/sdk-core/src/model/configuration/BacktraceConfiguration.ts @@ -3,7 +3,7 @@ import { BacktraceDatabaseConfiguration } from './BacktraceDatabaseConfiguration export interface BacktraceMetricsOptions { /** - * Metrics server hostname. By default the value is set to: https://events.backtrace.io. + * Metrics server hostname. By default the value is set to https://events.backtrace.io. */ metricsSubmissionUrl?: string; /** @@ -12,7 +12,7 @@ export interface BacktraceMetricsOptions { enable?: boolean; /** * Indicates how often crash free metrics are sent to Backtrace. The interval is a value in ms. - * By default, session events are sent on application startup/finish, and every 30 minutes while the game is running. + * By default, session events are sent on application startup/finish, and every 30 minutes while the application is running. * If the value is set to 0. The auto send mode is disabled. In this situation the application needs to maintain send * mode manually. */ diff --git a/packages/sdk-core/src/modules/metrics/BacktraceMetrics.ts b/packages/sdk-core/src/modules/metrics/BacktraceMetrics.ts index 4d88cd03..d2fada30 100644 --- a/packages/sdk-core/src/modules/metrics/BacktraceMetrics.ts +++ b/packages/sdk-core/src/modules/metrics/BacktraceMetrics.ts @@ -43,10 +43,7 @@ export class BacktraceMetrics { return; } - const { attributes } = this._attributeManager.get(); - this._summedEventsSubmissionQueue.add( - new SummedEvent('Application Launches', this.convertAttributes(attributes)), - ); + this.addSummedEvent('Application Launches'); this.send(); if (this._updateInterval === 0) { diff --git a/packages/sdk-core/src/modules/metrics/MetricsQueue.ts b/packages/sdk-core/src/modules/metrics/MetricsQueue.ts index 6facf220..628c3c00 100644 --- a/packages/sdk-core/src/modules/metrics/MetricsQueue.ts +++ b/packages/sdk-core/src/modules/metrics/MetricsQueue.ts @@ -1,7 +1,7 @@ export interface MetricsQueue { - total: number; - submissionUrl: string; - maximumEvents: number; + readonly total: number; + readonly submissionUrl: string; + readonly maximumEvents: number; add(event: T): void; send(): Promise; } diff --git a/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts b/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts index d7d3d128..2f4f62e9 100644 --- a/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts +++ b/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts @@ -13,7 +13,7 @@ export class MetricsSubmissionQueue implements MetricsQu return this._submissionUrl; } - public readonly TIMEOUT_BETWEEN_REQUESTS = TimeHelper.convertSecondsToMilliseconds(10); + public readonly DELAY_BETWEEN_REQUESTS = TimeHelper.convertSecondsToMilliseconds(10); private _events: T[] = []; private _numberOfDroppedRequests = 0; @@ -59,7 +59,7 @@ export class MetricsSubmissionQueue implements MetricsQu if (response.status !== 'Ok') { this._numberOfDroppedRequests++; attempts += 1; - await Delay.wait(2 ** attempts * this.TIMEOUT_BETWEEN_REQUESTS); + await Delay.wait(2 ** attempts * this.DELAY_BETWEEN_REQUESTS); await this.submitMetricsToServer(events, attempts); return; } From 74f8659a408a3662432fbaa74ab175ed7ee434f6 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Fri, 30 Jun 2023 11:46:36 +0200 Subject: [PATCH 10/13] Code review adjustements, test based on the variable and not random date, delay helper reject --- packages/browser/src/BacktraceBrowserSessionProvider.ts | 4 ++-- .../tests/metrics/persistentSessionProviderTests.spec.ts | 8 +++++--- packages/sdk-core/src/common/DelayHelper.ts | 4 ++-- .../src/modules/metrics/MetricsSubmissionQueue.ts | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/browser/src/BacktraceBrowserSessionProvider.ts b/packages/browser/src/BacktraceBrowserSessionProvider.ts index e9e078c7..0ac1c174 100644 --- a/packages/browser/src/BacktraceBrowserSessionProvider.ts +++ b/packages/browser/src/BacktraceBrowserSessionProvider.ts @@ -6,7 +6,7 @@ export class BacktraceBrowserSessionProvider implements BacktraceSessionProvider * Session persistence interval. If no event was send in the persistence interval * the session is treaten as an old session. */ - private readonly PERSISTENCE_INTERVAL = TimeHelper.convertSecondsToMilliseconds(30 * 60); + public static readonly PERSISTENCE_INTERVAL = TimeHelper.convertSecondsToMilliseconds(30 * 60); private readonly SESSION_LAST_ACTIVE = 'backtrace-last-active'; private readonly SESSION_GUID = 'backtrace-guid'; @@ -26,7 +26,7 @@ export class BacktraceBrowserSessionProvider implements BacktraceSessionProvider } const lastActive = this.readLastActiveTimestamp(); - if (!lastActive || TimeHelper.now() - lastActive > this.PERSISTENCE_INTERVAL) { + if (!lastActive || TimeHelper.now() - lastActive > BacktraceBrowserSessionProvider.PERSISTENCE_INTERVAL) { this.updateLastActiveTimestamp(); localStorage.setItem(this.SESSION_GUID, this.sessionId); return; diff --git a/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts b/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts index d75014ae..c532be0e 100644 --- a/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts +++ b/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts @@ -13,11 +13,13 @@ describe('Session provider tests', () => { expect(sessionProvider1.sessionId).toEqual(sessionProvider2.sessionId); }); - it('Should generate a new sessionId if the lastActive timestamp is greater than x', () => { + it('Should generate a new sessionId if the lastActive timestamp is greater than persistence interval time', () => { const fakeId = 'test'; - const sessionProvider = new BacktraceBrowserSessionProvider(); - localStorage.setItem('backtrace-last-active', new Date(2010, 1, 1, 1, 1, 1, 1).getTime().toString(10)); + const lastSessionActiveDate = new Date(Date.now() - BacktraceBrowserSessionProvider.PERSISTENCE_INTERVAL - 1); + localStorage.setItem('backtrace-last-active', lastSessionActiveDate.getTime().toString(10)); localStorage.setItem('backtrace-guid', fakeId); + + const sessionProvider = new BacktraceBrowserSessionProvider(); expect(sessionProvider.sessionId).toBeDefined(); expect(sessionProvider.sessionId).not.toEqual(fakeId); }); diff --git a/packages/sdk-core/src/common/DelayHelper.ts b/packages/sdk-core/src/common/DelayHelper.ts index 1686d790..5abb51bf 100644 --- a/packages/sdk-core/src/common/DelayHelper.ts +++ b/packages/sdk-core/src/common/DelayHelper.ts @@ -5,10 +5,10 @@ export class Delay { * @param signal abort signal */ public static wait(timeout: number, signal?: AbortSignal) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { function abortCallback() { clearTimeout(intervalId); - resolve(); + reject(); } const intervalId = setTimeout(() => { diff --git a/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts b/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts index 2f4f62e9..daeaf0c8 100644 --- a/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts +++ b/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts @@ -15,7 +15,7 @@ export class MetricsSubmissionQueue implements MetricsQu public readonly DELAY_BETWEEN_REQUESTS = TimeHelper.convertSecondsToMilliseconds(10); - private _events: T[] = []; + private readonly _events: T[] = []; private _numberOfDroppedRequests = 0; private readonly MAXIMUM_NUMBER_OF_ATTEMPTS = 3; @@ -73,7 +73,7 @@ export class MetricsSubmissionQueue implements MetricsQu } // push events to the beginning of the queue - this._events = events.concat(this._events); + this._events.unshift(...events); } private reachedLimit() { From 9b3592d4876830b6e054b8de122bcdb0dcaa2424 Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Fri, 30 Jun 2023 12:04:37 +0200 Subject: [PATCH 11/13] Recursive to iterative + delay helper error on reject --- packages/sdk-core/src/common/DelayHelper.ts | 2 +- .../modules/metrics/MetricsSubmissionQueue.ts | 42 +++++++++---------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/sdk-core/src/common/DelayHelper.ts b/packages/sdk-core/src/common/DelayHelper.ts index 5abb51bf..a283e837 100644 --- a/packages/sdk-core/src/common/DelayHelper.ts +++ b/packages/sdk-core/src/common/DelayHelper.ts @@ -8,7 +8,7 @@ export class Delay { return new Promise((resolve, reject) => { function abortCallback() { clearTimeout(intervalId); - reject(); + reject(new Error('Operation cancelled.')); } const intervalId = setTimeout(() => { diff --git a/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts b/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts index daeaf0c8..e213131c 100644 --- a/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts +++ b/packages/sdk-core/src/modules/metrics/MetricsSubmissionQueue.ts @@ -37,34 +37,32 @@ export class MetricsSubmissionQueue implements MetricsQu public async send() { const eventsToProcess = this._events.splice(0); - return await this.submitMetricsToServer(eventsToProcess); + return await this.submit(eventsToProcess); } - private async submitMetricsToServer(events: T[], attempts = 0) { - if (attempts >= this.MAXIMUM_NUMBER_OF_ATTEMPTS) { - this.returnEventsIfPossible(events); - return; - } + private async submit(events: T[]) { + for (let attempts = 0; attempts < this.MAXIMUM_NUMBER_OF_ATTEMPTS; attempts++) { + const response = await this._requestHandler.post( + this._submissionUrl, + JSON.stringify({ + ...this._metricMetadata, + [this._eventName]: events, + metadata: { + dropped_events: this._numberOfDroppedRequests, + }, + }), + ); + if (response.status === 'Ok') { + this._numberOfDroppedRequests = 0; + return; + } - const response = await this._requestHandler.post( - this._submissionUrl, - JSON.stringify({ - ...this._metricMetadata, - [this._eventName]: events, - metadata: { - dropped_events: this._numberOfDroppedRequests, - }, - }), - ); - if (response.status !== 'Ok') { this._numberOfDroppedRequests++; - attempts += 1; await Delay.wait(2 ** attempts * this.DELAY_BETWEEN_REQUESTS); - await this.submitMetricsToServer(events, attempts); - return; } - - this._numberOfDroppedRequests = 0; + // if the code reached this line, it means, we couldn't send data to server + // we need to try to return events to the queue and try to send it once again later. + this.returnEventsIfPossible(events); } private returnEventsIfPossible(events: T[]) { From 994ce10d050cc2d32b2e6e1b86c2c0434d5b0ecf Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Fri, 30 Jun 2023 12:09:41 +0200 Subject: [PATCH 12/13] Added one more test case --- examples/sdk/node/src/consts.ts | 2 +- examples/sdk/node/src/index.ts | 3 +++ .../metrics/persistentSessionProviderTests.spec.ts | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/examples/sdk/node/src/consts.ts b/examples/sdk/node/src/consts.ts index dd29bab4..5ae5b6aa 100644 --- a/examples/sdk/node/src/consts.ts +++ b/examples/sdk/node/src/consts.ts @@ -1,2 +1,2 @@ export const SUBMISSION_URL = - 'https://submit.backtrace.io/your-universe/0000000000000000000000000000000000000000000000000000000000000000/json'; + 'https://submit.backtrace.io/backtrace/46280f28e8b156a7816454ef07e94844ca23edafc306a2303a175db15aacbb17/json'; diff --git a/examples/sdk/node/src/index.ts b/examples/sdk/node/src/index.ts index 80f8ec48..2284fad2 100644 --- a/examples/sdk/node/src/index.ts +++ b/examples/sdk/node/src/index.ts @@ -13,6 +13,9 @@ const client = BacktraceClient.builder({ url: SUBMISSION_URL, attachments: [path.join(path.dirname(process.cwd()), 'samplefile.txt')], rateLimit: 5, + metrics: { + metricsSubmissionUrl: 'https://events-test.backtrace.io', + }, userAttributes: { 'custom-attribute': 'test', 'custom-annotation': { diff --git a/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts b/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts index c532be0e..4b4850ce 100644 --- a/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts +++ b/packages/browser/tests/metrics/persistentSessionProviderTests.spec.ts @@ -24,6 +24,17 @@ describe('Session provider tests', () => { expect(sessionProvider.sessionId).not.toEqual(fakeId); }); + it('Should not generate a new sessionId if the lastActive timestamp is lower than persistence interval time', () => { + const fakeId = 'test'; + const lastSessionActiveDate = new Date(Date.now() - BacktraceBrowserSessionProvider.PERSISTENCE_INTERVAL + 1); + localStorage.setItem('backtrace-last-active', lastSessionActiveDate.getTime().toString(10)); + localStorage.setItem('backtrace-guid', fakeId); + + const sessionProvider = new BacktraceBrowserSessionProvider(); + expect(sessionProvider.sessionId).toBeDefined(); + expect(sessionProvider.sessionId).toEqual(fakeId); + }); + it('Should update timestamp', () => { const timestamp = Date.now(); jest.spyOn(TimeHelper, 'now').mockImplementation(() => { From f13043991ab9b644ddde4149fa18c42cb115e3ef Mon Sep 17 00:00:00 2001 From: Konrad Dysput Date: Fri, 30 Jun 2023 12:12:41 +0200 Subject: [PATCH 13/13] Rollback submission url in the test --- examples/sdk/node/src/consts.ts | 2 +- examples/sdk/node/src/index.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/sdk/node/src/consts.ts b/examples/sdk/node/src/consts.ts index 5ae5b6aa..dd29bab4 100644 --- a/examples/sdk/node/src/consts.ts +++ b/examples/sdk/node/src/consts.ts @@ -1,2 +1,2 @@ export const SUBMISSION_URL = - 'https://submit.backtrace.io/backtrace/46280f28e8b156a7816454ef07e94844ca23edafc306a2303a175db15aacbb17/json'; + 'https://submit.backtrace.io/your-universe/0000000000000000000000000000000000000000000000000000000000000000/json'; diff --git a/examples/sdk/node/src/index.ts b/examples/sdk/node/src/index.ts index 2284fad2..80f8ec48 100644 --- a/examples/sdk/node/src/index.ts +++ b/examples/sdk/node/src/index.ts @@ -13,9 +13,6 @@ const client = BacktraceClient.builder({ url: SUBMISSION_URL, attachments: [path.join(path.dirname(process.cwd()), 'samplefile.txt')], rateLimit: 5, - metrics: { - metricsSubmissionUrl: 'https://events-test.backtrace.io', - }, userAttributes: { 'custom-attribute': 'test', 'custom-annotation': {