diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 1f69b3b9e..82a8ab2bd 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -8,6 +8,7 @@ import LRU from 'lru-cache' import pMemoize from 'p-memoize' import toMultiaddr from 'uri-to-multiaddr' import browser from 'webextension-polyfill' +import { handleConsentFromState, trackView } from '../lib/telemetry.js' import { contextMenuCopyAddressAtPublicGw, contextMenuCopyCanonicalAddress, contextMenuCopyCidAddress, contextMenuCopyPermalink, contextMenuCopyRawCid, contextMenuViewOnGateway, createContextMenus, findValueForContext } from './context-menus.js' import createCopier from './copier.js' import createDnslinkResolver from './dnslink.js' @@ -24,7 +25,6 @@ import { guiURLString, migrateOptions, optionDefaults, safeURL, storeMissingOpti import { getExtraInfoSpec } from './redirect-handler/blockOrObserve.js' import createRuntimeChecks from './runtime-checks.js' import { initState, offlinePeerCount } from './state.js' -import { handleConsentFromState, trackView } from '../lib/telemetry.js' // this won't work in webworker context. Needs to be enabled manually // https://github.com/debug-js/debug/issues/916 diff --git a/add-on/src/lib/ipfs-request.js b/add-on/src/lib/ipfs-request.js index 480870ffa..2a400d1f7 100644 --- a/add-on/src/lib/ipfs-request.js +++ b/add-on/src/lib/ipfs-request.js @@ -3,14 +3,15 @@ import debug from 'debug' -import LRU from 'lru-cache' -import isIPFS from 'is-ipfs' import isFQDN from 'is-fqdn' +import isIPFS from 'is-ipfs' +import LRU from 'lru-cache' +import { recoveryPagePath } from './constants.js' +import { braveNodeType } from './ipfs-client/brave.js' import { dropSlash, ipfsUri, pathAtHttpGateway, sameGateway } from './ipfs-path.js' import { safeURL } from './options.js' -import { braveNodeType } from './ipfs-client/brave.js' -import { recoveryPagePath } from './constants.js' import { addRuleToDynamicRuleSetGenerator, isLocalHost, supportsBlock } from './redirect-handler/blockOrObserve.js' +import { RequestTracker } from './trackers/requestTracker.js' const log = debug('ipfs-companion:request') log.error = debug('ipfs-companion:request:error') @@ -32,6 +33,8 @@ const recoverableHttpError = (code) => code && code >= 400 // Tracking late redirects for edge cases such as https://github.com/ipfs-shipyard/ipfs-companion/issues/436 const onHeadersReceivedRedirect = new Set() let addRuleToDynamicRuleSet = null +const observedRequestTracker = new RequestTracker('url-observed') +const resolvedRequestTracker = new RequestTracker('url-resolved') // Request modifier provides event listeners for the various stages of making an HTTP request // API Details: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest @@ -144,6 +147,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida async onBeforeRequest (request) { const state = getState() if (!state.active) return + observedRequestTracker.track(request) // When local IPFS node is unreachable , show recovery page where user can redirect // to public gateway. @@ -151,7 +155,8 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida const publicUri = await ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString) return handleRedirection({ originUrl: request.url, - redirectUrl: `${dropSlash(runtimeRoot)}${recoveryPagePath}#${encodeURIComponent(publicUri)}` + redirectUrl: `${dropSlash(runtimeRoot)}${recoveryPagePath}#${encodeURIComponent(publicUri)}`, + request }) } @@ -162,7 +167,8 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida const redirectUrl = safeURL(request.url, { useLocalhostName: state.useSubdomains }).toString() return handleRedirection({ originUrl: request.url, - redirectUrl + redirectUrl, + request }) } @@ -171,7 +177,8 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida const redirectUrl = safeURL(request.url, { useLocalhostName: false }).toString() return handleRedirection({ originUrl: request.url, - redirectUrl + redirectUrl, + request }) } @@ -478,8 +485,9 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida * @param {object} input contains originUrl and redirectUrl. * @returns */ -function handleRedirection ({ originUrl, redirectUrl }) { +function handleRedirection ({ originUrl, redirectUrl, request }) { if (redirectUrl !== '' && originUrl !== '' && redirectUrl !== originUrl) { + resolvedRequestTracker.track(request) if (supportsBlock) { return { redirectUrl } } @@ -538,7 +546,8 @@ async function redirectToGateway (request, url, state, ipfsPathValidator, runtim return handleRedirection({ originUrl: request.url, - redirectUrl + redirectUrl, + request }) } @@ -608,7 +617,8 @@ function normalizedRedirectingProtocolRequest (request, pubGwUrl) { if (oldPath !== path && isIPFS.path(path)) { return handleRedirection({ originUrl: request.url, - redirectUrl: pathAtHttpGateway(path, pubGwUrl) + redirectUrl: pathAtHttpGateway(path, pubGwUrl), + request }) } return null @@ -653,7 +663,9 @@ function normalizedUnhandledIpfsProtocol (request, pubGwUrl) { // (will be redirected later, if needed) return handleRedirection({ originUrl: request.url, - redirectUrl: pathAtHttpGateway(path, pubGwUrl) + redirectUrl: pathAtHttpGateway(path, pubGwUrl), + request + }) } } diff --git a/add-on/src/lib/telemetry.ts b/add-on/src/lib/telemetry.ts index a55c69eee..6b3eca5f0 100644 --- a/add-on/src/lib/telemetry.ts +++ b/add-on/src/lib/telemetry.ts @@ -4,6 +4,7 @@ import debug from 'debug' import { WebExtensionStorageProvider } from './storage-provider/WebExtensionStorageProvider.js' import { CompanionState } from '../types/companion.js' import { consentTypes } from '@ipfs-shipyard/ignite-metrics' +import type { CountlyEvent } from 'countly-web-sdk' const log = debug('ipfs-companion:telemetry') @@ -51,3 +52,14 @@ export function trackView (view: string, segments: Record): void log('trackView called for view: ', view) metricsProvider.trackView(view, ignoredViewsRegex, segments) } + +/** + * TrackView is a wrapper around ignite-metrics trackView + * + * @param event + * @param segments + */ +export function trackEvent (event: CountlyEvent): void { + log('trackEvent called for event: ', event) + metricsProvider.trackEvent(event) +} diff --git a/add-on/src/lib/trackers/requestTracker.ts b/add-on/src/lib/trackers/requestTracker.ts new file mode 100644 index 000000000..4f3a97009 --- /dev/null +++ b/add-on/src/lib/trackers/requestTracker.ts @@ -0,0 +1,49 @@ +import debug from 'debug' +import type browser from 'webextension-polyfill' +import { trackEvent } from '../telemetry.js' + +export class RequestTracker { + private readonly eventKey: 'url-observed' | 'url-resolved' + private readonly flushInterval: number + private readonly log: debug.Debugger & { error?: debug.Debugger } + private lastSync: number = Date.now() + private requestTypeStore: { [key in browser.WebRequest.ResourceType]?: number } = {} + + constructor (eventKey: 'url-observed' | 'url-resolved', flushInterval = 1000 * 60 * 5) { + this.eventKey = eventKey + this.log = debug(`ipfs-companion:request-tracker:${eventKey}`) + this.log.error = debug(`ipfs-companion:request-tracker:${eventKey}:error`) + this.flushInterval = flushInterval + this.setupFlushScheduler() + } + + track ({ type }: browser.WebRequest.OnBeforeRequestDetailsType): void { + this.log(`track ${type}`, JSON.stringify(this.requestTypeStore)) + this.requestTypeStore[type] = (this.requestTypeStore[type] ?? 0) + 1 + } + + private flushStore (): void { + this.log('flushing') + const count = Object.values(this.requestTypeStore).reduce((a, b): number => a + b, 0) + if (count === 0) { + this.log('nothing to flush') + return + } + trackEvent({ + key: this.eventKey, + count, + dur: Date.now() - this.lastSync, + segmentation: Object.assign({}, this.requestTypeStore) as unknown as Record + }) + // reset + this.lastSync = Date.now() + this.requestTypeStore = {} + } + + private setupFlushScheduler (): void { + setTimeout(() => { + this.flushStore() + this.setupFlushScheduler() + }, this.flushInterval) + } +} diff --git a/add-on/src/types/countly.d.ts b/add-on/src/types/countly.d.ts new file mode 100644 index 000000000..30eedfa6f --- /dev/null +++ b/add-on/src/types/countly.d.ts @@ -0,0 +1,9 @@ +declare module 'countly-web-sdk' { + export interface CountlyEvent { + key: string + count?: number + sum?: number + dur?: number + segmentation?: Record + } +} diff --git a/docs/telemetry/COLLECTED_DATA.md b/docs/telemetry/COLLECTED_DATA.md index 56e1570c4..ffb970f7d 100644 --- a/docs/telemetry/COLLECTED_DATA.md +++ b/docs/telemetry/COLLECTED_DATA.md @@ -17,7 +17,7 @@ Telemetry is sent to Countly instance at `countly.ipfs.tech`. You can read how t As a general rule, we collect only application data; no user data. Some examples of application data we collect are: | Metric data name | Metric feature name | Metric trigger | Analytics use | Notes | -|:-------------------:|---------------------|---------------------------------------------------------|-------------------------------------------------------------------|---------------| | | +|:-------------------:|---------------------|---------------------------------------------------------|-------------------------------------------------------------------|---------------| | view:welcome | views | When the welcome view is shown | View count | | | view:options | views | When the options view is shown | View count | | | view:quick-import | views | When the quick-import view is shown | View count | | diff --git a/patches/countly-sdk-web+23.2.2.patch b/patches/countly-sdk-web+23.2.2.patch index a3f99907e..194da1e4d 100644 --- a/patches/countly-sdk-web+23.2.2.patch +++ b/patches/countly-sdk-web+23.2.2.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/countly-sdk-web/lib/countly.js b/node_modules/countly-sdk-web/lib/countly.js -index da26eb6..53c31d0 100644 +index da26eb6..0bf0ec8 100644 --- a/node_modules/countly-sdk-web/lib/countly.js +++ b/node_modules/countly-sdk-web/lib/countly.js @@ -52,10 +52,22 @@ @@ -27,7 +27,7 @@ index da26eb6..53c31d0 100644 // TODO: check if logging can be added here, like: // console.error("Not running in browser"); return; -@@ -347,12 +359,12 @@ +@@ -347,23 +359,16 @@ checkIgnore(); @@ -39,11 +39,23 @@ index da26eb6..53c31d0 100644 } catch (ex) { - log(logLevelEnums.ERROR, "initialize, Could not parse name: " + window.name + ", error: " + ex); +- } +- } +- else if (location.hash && location.hash.indexOf("#cly:") === 0) { +- try { +- this.passed_data = JSON.parse(location.hash.replace("#cly:", "")); +- } +- catch (ex) { +- log(logLevelEnums.ERROR, "initialize, Could not parse hash: " + location.hash + ", error: " + ex); + log(logLevelEnums.ERROR, "initialize, Could not parse name: " + globalThis.name + ", error: " + ex); } } - else if (location.hash && location.hash.indexOf("#cly:") === 0) { -@@ -1028,7 +1040,7 @@ + ++ + if ((this.passed_data && this.passed_data.app_key && this.passed_data.app_key === this.app_key) || (this.passed_data && !this.passed_data.app_key && global)) { + if (this.passed_data.token && this.passed_data.purpose) { + if (this.passed_data.token !== getValueFromStorage("cly_old_token")) { +@@ -1028,7 +1033,7 @@ if (this.enableOrientationTracking) { // report orientation this.report_orientation(); @@ -52,7 +64,7 @@ index da26eb6..53c31d0 100644 self.report_orientation(); }); } -@@ -1618,11 +1630,11 @@ +@@ -1618,11 +1623,11 @@ log(logLevelEnums.INFO, "track_errors, Started tracking errors"); // Indicated that for this instance of the countly error tracking is enabled Countly.i[this.app_key].tracking_crashes = true; @@ -67,7 +79,7 @@ index da26eb6..53c31d0 100644 // old browsers like IE 10 and Safari 9 won't give this value 'err' to us, but if it is provided we can trigger error recording immediately if (err !== undefined && err !== null) { // false indicates fatal error (as in non_fatal:false) -@@ -1630,7 +1642,7 @@ +@@ -1630,7 +1635,7 @@ } // fallback if no error object is present for older browsers, we create it instead else { @@ -76,7 +88,7 @@ index da26eb6..53c31d0 100644 var error = ""; if (typeof msg !== "undefined") { error += msg + "\n"; -@@ -1666,7 +1678,7 @@ +@@ -1666,7 +1671,7 @@ }; // error handling for 'uncaught rejections' @@ -85,7 +97,7 @@ index da26eb6..53c31d0 100644 // true indicates non fatal error (as in non_fatal: true) dispatchErrors(new Error("Unhandled rejection (reason: " + (event.reason && event.reason.stack ? event.reason.stack : event.reason) + ")."), true); }); -@@ -1909,13 +1921,13 @@ +@@ -1909,13 +1914,13 @@ this.begin_session(); this.start_time(); // end session on unload @@ -101,7 +113,7 @@ index da26eb6..53c31d0 100644 var hidden = "hidden"; /** -@@ -1931,17 +1943,17 @@ +@@ -1931,17 +1936,17 @@ } // add focus handling eventListeners @@ -125,7 +137,7 @@ index da26eb6..53c31d0 100644 } // Page Visibility API for changing tabs and minimizing browser -@@ -1971,10 +1983,10 @@ +@@ -1971,10 +1976,10 @@ inactivityCounter = 0; } @@ -140,7 +152,7 @@ index da26eb6..53c31d0 100644 // track user inactivity setInterval(function() { -@@ -2048,7 +2060,7 @@ +@@ -2048,7 +2053,7 @@ // truncate new segment segments = truncateObject(segments, self.maxKeyLength, self.maxValueSize, self.maxSegmentationValues, "track_pageview", log); if (this.track_domains) { @@ -149,7 +161,7 @@ index da26eb6..53c31d0 100644 } if (useSessionCookie) { -@@ -2071,7 +2083,7 @@ +@@ -2071,7 +2076,7 @@ else if (typeof document.referrer !== "undefined" && document.referrer.length) { var matches = urlParseRE.exec(document.referrer); // do not report referrers of current website @@ -158,7 +170,7 @@ index da26eb6..53c31d0 100644 segments.start = 1; } } -@@ -2147,7 +2159,7 @@ +@@ -2147,7 +2152,7 @@ // truncate new segment segments = truncateObject(segments, self.maxKeyLength, self.maxValueSize, self.maxSegmentationValues, "processClick", log); if (self.track_domains) { @@ -167,7 +179,7 @@ index da26eb6..53c31d0 100644 } add_cly_events({ key: internalEventKeyEnums.ACTION, -@@ -2173,7 +2185,7 @@ +@@ -2173,7 +2178,7 @@ if (parent) { log(logLevelEnums.INFO, "track_scrolls, Tracking the specified children"); } @@ -176,7 +188,7 @@ index da26eb6..53c31d0 100644 isScrollRegistryOpen = true; trackingScrolls = true; -@@ -2813,8 +2825,8 @@ +@@ -2813,8 +2818,8 @@ else { var pages = widgets[i].target_pages; for (var k = 0; k < pages.length; k++) { @@ -187,7 +199,7 @@ index da26eb6..53c31d0 100644 var isContainAsterisk = pages[k].includes("*"); if (((isContainAsterisk && isWildcardMatched) || isFullPathMatched) && !widgets[i].hide_sticker) { processWidget(widgets[i], true); -@@ -3048,7 +3060,7 @@ +@@ -3048,7 +3053,7 @@ return; } @@ -196,7 +208,7 @@ index da26eb6..53c31d0 100644 var feedbackWidgetFamily; // set feedback widget family as ratings and load related style file when type is ratings -@@ -3170,7 +3182,7 @@ +@@ -3170,7 +3175,7 @@ wrapper.appendChild(iframe); log(logLevelEnums.DEBUG, "present_feedback_widget, Appended the iframe"); @@ -205,7 +217,7 @@ index da26eb6..53c31d0 100644 var data = {}; try { data = JSON.parse(e.data); -@@ -3249,9 +3261,9 @@ +@@ -3249,9 +3254,9 @@ break; case "onScrollHalfwayDown": @@ -217,7 +229,7 @@ index da26eb6..53c31d0 100644 var documentHeight = getDocHeight(); if (scrollY >= (documentHeight / 2)) { surveyShown = true; -@@ -3841,13 +3853,13 @@ +@@ -3841,13 +3846,13 @@ var height = (screen.height) ? parseInt(screen.height) : 0; if (width !== 0 && height !== 0) { var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); @@ -235,7 +247,7 @@ index da26eb6..53c31d0 100644 // we have landscape orientation // switch values for all except ios var temp = width; -@@ -3860,8 +3872,8 @@ +@@ -3860,8 +3865,8 @@ } // getting density ratio @@ -246,7 +258,7 @@ index da26eb6..53c31d0 100644 } // getting locale -@@ -3873,7 +3885,7 @@ +@@ -3873,7 +3878,7 @@ if (typeof document.referrer !== "undefined" && document.referrer.length) { var matches = urlParseRE.exec(document.referrer); // do not report referrers of current website @@ -255,7 +267,7 @@ index da26eb6..53c31d0 100644 var ignoring = false; if (ignoreReferrers && ignoreReferrers.length) { for (var k = 0; k < ignoreReferrers.length; k++) { -@@ -3907,7 +3919,7 @@ +@@ -3907,7 +3912,7 @@ * @memberof Countly._internals */ function log(level, message) { @@ -264,7 +276,7 @@ index da26eb6..53c31d0 100644 // parse the arguments into a string if it is an object if (arguments[2] && typeof arguments[2] === "object") { arguments[2] = JSON.stringify(arguments[2]); -@@ -3963,72 +3975,62 @@ +@@ -3963,72 +3968,62 @@ */ function sendXmlHttpRequest(functionName, url, params, callback, useBroadResponseValidator) { useBroadResponseValidator = useBroadResponseValidator || false; @@ -364,12 +376,12 @@ index da26eb6..53c31d0 100644 - }; - if (method === "GET") { - xhr.send(); +- } +- else { +- xhr.send(data); + } else { + throw new Error(response.statusText); } -- else { -- xhr.send(data); -- } - } - catch (e) { + }).catch(function (e) { @@ -384,7 +396,7 @@ index da26eb6..53c31d0 100644 } /** -@@ -4105,7 +4107,7 @@ +@@ -4105,7 +4100,7 @@ * */ function processScroll() { @@ -393,7 +405,7 @@ index da26eb6..53c31d0 100644 } /** -@@ -4131,7 +4133,7 @@ +@@ -4131,7 +4126,7 @@ // truncate new segment segments = truncateObject(segments, self.maxKeyLength, self.maxValueSize, self.maxSegmentationValues, "processScrollView", log); if (self.track_domains) { @@ -402,7 +414,7 @@ index da26eb6..53c31d0 100644 } add_cly_events({ key: internalEventKeyEnums.ACTION, -@@ -4862,7 +4864,7 @@ +@@ -4862,7 +4857,7 @@ */ var get_event_target = function(event) { if (!event) { @@ -411,7 +423,7 @@ index da26eb6..53c31d0 100644 } if (typeof event.target !== "undefined") { return event.target; -@@ -4924,7 +4926,7 @@ +@@ -4924,7 +4919,7 @@ var device = "desktop"; // regexps corresponding to tablets or phones that can be found in userAgent string @@ -420,7 +432,7 @@ index da26eb6..53c31d0 100644 var phoneCheck = /(mobi|ipod|phone|blackberry|opera mini|fennec|minimo|symbian|psp|nintendo ds|archos|skyfire|puffin|blazer|bolt|gobrowser|iris|maemo|semc|teashark|uzard)/; // check whether the regexp values corresponds to something in the user agent string -@@ -5009,7 +5011,7 @@ +@@ -5009,7 +5004,7 @@ return Math.min( Math.min(D.body.clientHeight, D.documentElement.clientHeight), Math.min(D.body.offsetHeight, D.documentElement.offsetHeight), @@ -429,7 +441,7 @@ index da26eb6..53c31d0 100644 ); } -@@ -5018,13 +5020,13 @@ +@@ -5018,22 +5013,9 @@ * @returns {String} device orientation */ function getOrientation() { @@ -437,15 +449,23 @@ index da26eb6..53c31d0 100644 + return globalThis.innerWidth > globalThis.innerHeight ? "landscape" : "portrait"; } - /** - * Monitor parallel storage changes like other opened tabs - */ +- /** +- * Monitor parallel storage changes like other opened tabs +- */ - window.addEventListener("storage", function(e) { -+ globalThis.addEventListener("storage", function(e) { - var parts = (e.key + "").split("/"); - var key = parts.pop(); - var appKey = parts.pop(); -@@ -5171,7 +5173,7 @@ +- var parts = (e.key + "").split("/"); +- var key = parts.pop(); +- var appKey = parts.pop(); +- +- if (Countly.i && Countly.i[appKey]) { +- Countly.i[appKey].onStorageChange(key, e.newValue); +- } +- }); +- + /** + * Load external js files + * @param {String} tag - Tag/node name to load file in +@@ -5171,7 +5153,7 @@ * @return {string} view name * */ Countly.getViewName = function() { @@ -454,7 +474,7 @@ index da26eb6..53c31d0 100644 }; /** -@@ -5179,7 +5181,7 @@ +@@ -5179,7 +5161,7 @@ * @return {string} view url * */ Countly.getViewUrl = function() { @@ -463,12 +483,12 @@ index da26eb6..53c31d0 100644 }; /** -@@ -5187,7 +5189,7 @@ +@@ -5187,7 +5169,7 @@ * @return {string} view url * */ Countly.getSearchQuery = function() { - return window.location.search; -+ return globalThis.location.search; ++ return ''; }; /** diff --git a/test/functional/lib/trackers/requestTrackers.test.ts b/test/functional/lib/trackers/requestTrackers.test.ts new file mode 100644 index 000000000..0e0b936c1 --- /dev/null +++ b/test/functional/lib/trackers/requestTrackers.test.ts @@ -0,0 +1,116 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import browser from 'sinon-chrome'; +import PatchedCountly from 'countly-sdk-web' +import { RequestTracker } from './../../../../add-on/src/lib/trackers/requestTracker.js' + +const sinonSandBox = sinon.createSandbox() +describe('lib/trackers/requestTracker', () => { + + let requestTracker: RequestTracker + let countlySDKStub: sinon.SinonStub + let clock: sinon.SinonFakeTimers + + before(() => { + clock = sinonSandBox.useFakeTimers() + countlySDKStub = sinonSandBox.stub(PatchedCountly) + }) + + afterEach(() => { + sinonSandBox.resetHistory() + }) + + describe('url-observed', () => { + before(() => { + requestTracker = new RequestTracker('url-observed') + }) + + it('should init a Tracker', () => { + expect(requestTracker).to.be.instanceOf(RequestTracker) + expect(requestTracker).to.have.property('track') + }) + + it('should track a request', async () => { + await requestTracker.track({ type: 'main_frame' } as browser.WebRequest.OnBeforeRequestDetailsType) + clock.tick(1000 * 60 * 6) + sinon.assert.calledWith(countlySDKStub.add_event, { + key: 'url-observed', + count: 1, + dur: 300000, + segmentation: { + main_frame: 1 + } + }) + }) + + it('should track multiple requests', async () => { + await requestTracker.track({ type: 'main_frame' } as browser.WebRequest.OnBeforeRequestDetailsType) + await requestTracker.track({ type: 'sub_frame' } as browser.WebRequest.OnBeforeRequestDetailsType) + await requestTracker.track({ type: 'xmlHTTPRequest' } as browser.WebRequest.OnBeforeRequestDetailsType) + clock.tick(1000 * 60 * 6) + sinon.assert.calledWith(countlySDKStub.add_event, { + key: 'url-observed', + count: 3, + dur: 300000, + segmentation: { + main_frame: 1, + sub_frame: 1, + xmlHTTPRequest: 1 + } + }) + }) + + it('should not send event if count is 0', async () => { + clock.tick(1000 * 60 * 6) + + sinon.assert.notCalled(countlySDKStub.add_event) + }) + }) + + describe('url-resolved', () => { + before(() => { + requestTracker = new RequestTracker('url-resolved') + }) + + it('should init a Tracker', () => { + expect(requestTracker).to.be.instanceOf(RequestTracker) + expect(requestTracker).to.have.property('track') + }) + + it('should track a request', async () => { + await requestTracker.track({ type: 'main_frame' } as browser.WebRequest.OnBeforeRequestDetailsType) + clock.tick(1000 * 60 * 6) + sinon.assert.calledWith(countlySDKStub.add_event, { + key: 'url-resolved', + count: 1, + dur: 300000, + segmentation: { + main_frame: 1 + } + }) + }) + + it('should track multiple requests', async () => { + await requestTracker.track({ type: 'main_frame' } as browser.WebRequest.OnBeforeRequestDetailsType) + await requestTracker.track({ type: 'sub_frame' } as browser.WebRequest.OnBeforeRequestDetailsType) + await requestTracker.track({ type: 'xmlHTTPRequest' } as browser.WebRequest.OnBeforeRequestDetailsType) + clock.tick(1000 * 60 * 6) + sinon.assert.calledWith(countlySDKStub.add_event, { + key: 'url-resolved', + count: 3, + dur: 300000, + segmentation: { + main_frame: 1, + sub_frame: 1, + xmlHTTPRequest: 1 + } + }) + }) + + it('should not send event if count is 0', async () => { + clock.tick(1000 * 60 * 6) + + sinon.assert.notCalled(countlySDKStub.add_event) + }) + }) +})