diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd09f8..caaa90c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### Added +- Google Analytics improvements. +### Security +- Updated versions of vulnerable packages (glob (minimatch ReDoS - [CWE-1333](https://cwe.mitre.org/data/definitions/1333.html), [CWE-407](https://cwe.mitre.org/data/definitions/407.html)), axios - [CVE-2026-25639](https://www.cve.org/CVERecord?id=CVE-2026-25639), diff, lodash). ## [5.5.10] - 2026-02-05 ### Fixed diff --git a/VERSION b/VERSION index 030df7b..864fffe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.5.10 +5.5.11-SNAPSHOT diff --git a/__tests__/report-portal-client.spec.js b/__tests__/report-portal-client.spec.js index e0ced6e..4754279 100644 --- a/__tests__/report-portal-client.spec.js +++ b/__tests__/report-portal-client.spec.js @@ -158,6 +158,7 @@ describe('ReportPortal javascript client', () => { endpoint: 'https://rp.us/api/v1', project: 'tst', }); + jest.spyOn(client, 'fetchServerInfo').mockResolvedValue({}); jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); await client.triggerStatisticsEvent(); @@ -218,6 +219,101 @@ describe('ReportPortal javascript client', () => { }), ); }); + + it('should fetch server info and set instanceID before tracking event', async () => { + const client = new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }); + const serverInfoResponse = { + extensions: { + result: { + 'server.details.instance': 'test-instance-123', + }, + }, + }; + jest.spyOn(client, 'fetchServerInfo').mockResolvedValue(serverInfoResponse); + jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); + jest.spyOn(client.statistics, 'setInstanceID'); + + await client.triggerStatisticsEvent(); + + expect(client.fetchServerInfo).toHaveBeenCalled(); + expect(client.statistics.setInstanceID).toHaveBeenCalledWith('test-instance-123'); + expect(client.statistics.trackEvent).toHaveBeenCalled(); + }); + + it('should still track event with not_set instanceID if fetchServerInfo fails', async () => { + const client = new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }); + jest.spyOn(client, 'fetchServerInfo').mockRejectedValue(new Error('Network error')); + jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); + jest.spyOn(client.statistics, 'setInstanceID'); + + await client.triggerStatisticsEvent(); + + expect(client.statistics.setInstanceID).toHaveBeenCalledWith('not_set'); + expect(client.statistics.trackEvent).toHaveBeenCalled(); + }); + + it('should not set instanceID if server info does not contain it', async () => { + const client = new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }); + jest.spyOn(client, 'fetchServerInfo').mockResolvedValue({}); + jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); + jest.spyOn(client.statistics, 'setInstanceID'); + + await client.triggerStatisticsEvent(); + + expect(client.statistics.setInstanceID).not.toHaveBeenCalled(); + expect(client.statistics.trackEvent).toHaveBeenCalled(); + }); + }); + + describe('getServerInfoUrl', () => { + it('should return correct info URL for v1 endpoint', () => { + const client = new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://rp.us/api/v1', + }); + + expect(client.getServerInfoUrl()).toBe('https://rp.us/api/info'); + }); + + it('should return correct info URL for v2 endpoint', () => { + const client = new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://rp.us/api/v2', + }); + + expect(client.getServerInfoUrl()).toBe('https://rp.us/api/info'); + }); + }); + + describe('fetchServerInfo', () => { + it('should call restClient.request with correct URL', async () => { + const client = new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://rp.us/api/v1', + }); + const serverInfo = { extensions: { result: {} } }; + jest.spyOn(client.restClient, 'request').mockResolvedValue(serverInfo); + + const result = await client.fetchServerInfo(); + + expect(client.restClient.request).toHaveBeenCalledWith('GET', 'https://rp.us/api/info', {}); + expect(result).toEqual(serverInfo); + }); }); describe('startLaunch', () => { diff --git a/__tests__/statistics.spec.js b/__tests__/statistics.spec.js index 12c4e95..23d66ee 100644 --- a/__tests__/statistics.spec.js +++ b/__tests__/statistics.spec.js @@ -91,4 +91,43 @@ describe('Statistics', () => { expect(console.error).toHaveBeenCalledWith(errorMessage); }); }); + + describe('setInstanceID', () => { + it('should set instanceID in event params', async () => { + jest.spyOn(axios, 'post').mockReturnValue({ + send: () => {}, // eslint-disable-line + }); + + const statistics = new Statistics(eventName, agentParams); + statistics.setInstanceID('test-instance-id'); + await statistics.trackEvent(); + + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalledWith( + url, + expect.objectContaining({ + events: expect.arrayContaining([ + expect.objectContaining({ + params: expect.objectContaining({ + instanceID: 'test-instance-id', + }), + }), + ]), + }), + ); + }); + + it('should not include instanceID if setInstanceID was not called', async () => { + jest.spyOn(axios, 'post').mockReturnValue({ + send: () => {}, // eslint-disable-line + }); + + const statistics = new Statistics(eventName, agentParams); + await statistics.trackEvent(); + + expect(axios.post).toHaveBeenCalledTimes(1); + const callArgs = axios.post.mock.calls[0][1]; + expect(callArgs.events[0].params).not.toHaveProperty('instanceID'); + }); + }); }); diff --git a/index.d.ts b/index.d.ts index a361569..fedd35d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -268,7 +268,7 @@ declare module '@reportportal/client-javascript' { /** * Initializes a new Report Portal client. */ - constructor(config: ReportPortalConfig, agentInfo?: { name?: string; version?: string }); + constructor(config: ReportPortalConfig, agentInfo?: { name?: string; version?: string; framework_version?: string }); /** * Starts a new launch. diff --git a/lib/report-portal-client.js b/lib/report-portal-client.js index 289aa76..7adbb25 100644 --- a/lib/report-portal-client.js +++ b/lib/report-portal-client.js @@ -146,10 +146,28 @@ class RPClient { return this.restClient.request('GET', url, {}); } + getServerInfoUrl() { + return this.config.endpoint.replace('/v1', '/info').replace('/v2', '/info'); + } + + async fetchServerInfo() { + const url = this.getServerInfoUrl(); + return this.restClient.request('GET', url, {}); + } + async triggerStatisticsEvent() { if (process.env.REPORTPORTAL_CLIENT_JS_NO_ANALYTICS) { return; } + try { + const serverInfo = await this.fetchServerInfo(); + const instanceID = serverInfo?.extensions?.result?.['server.details.instance']; + if (instanceID) { + this.statistics.setInstanceID(instanceID); + } + } catch (e) { + this.statistics.setInstanceID('not_set'); + } await this.statistics.trackEvent(); } diff --git a/package-lock.json b/package-lock.json index 9d1bd09..6408d15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,12 +6,12 @@ "packages": { "": { "name": "@reportportal/client-javascript", - "version": "5.5.8", + "version": "5.5.10", "license": "Apache-2.0", "dependencies": { - "axios": "^1.12.2", + "axios": "^1.13.5", "axios-retry": "^4.5.0", - "glob": "^13.0.1", + "glob": "^13.0.6", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "ini": "^2.0.0", @@ -802,27 +802,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2014,13 +1993,13 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -3731,9 +3710,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3924,17 +3903,17 @@ } }, "node_modules/glob": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", - "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3953,16 +3932,37 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5787,10 +5787,10 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -6174,26 +6174,26 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "license": "ISC", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.0.tgz", + "integrity": "sha512-sr8xPKE25m6vJVcrdn6NxtC0fVfuPowbscLypegRgOm0yXSqr5JNHCAY3hnusdJ7HRBW04j6Ip4khvHU778DuQ==", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } diff --git a/package.json b/package.json index b1b6dc6..60e02d9 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,9 @@ "node": ">=14.x" }, "dependencies": { - "axios": "^1.12.2", + "axios": "^1.13.5", "axios-retry": "^4.5.0", - "glob": "^13.0.1", + "glob": "^13.0.6", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "ini": "^2.0.0", diff --git a/statistics/statistics.js b/statistics/statistics.js index 56042ee..67c0133 100644 --- a/statistics/statistics.js +++ b/statistics/statistics.js @@ -24,9 +24,20 @@ class Statistics { if (agentParams && hasOption(agentParams, 'version') && agentParams.version) { params.agent_version = agentParams.version; } + if ( + agentParams && + hasOption(agentParams, 'framework_version') && + agentParams.framework_version + ) { + params.framework_version = agentParams.framework_version; + } return params; } + setInstanceID(instanceID) { + this.eventParams.instanceID = instanceID; + } + async trackEvent() { try { const requestBody = {