Compare commits
10 commits
main
...
tracker-sd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53cf91bb16 | ||
|
|
fa2923b83d | ||
|
|
529965486c | ||
|
|
9f51ab85da | ||
|
|
226fc867c0 | ||
|
|
2a1c28cc49 | ||
|
|
4c967d4bc1 | ||
|
|
3fdf799bd7 | ||
|
|
9aca716e6b | ||
|
|
cf9ecdc9a4 |
30 changed files with 1589 additions and 71 deletions
|
|
@ -135,11 +135,6 @@ func (e *handlersImpl) startSessionHandlerWeb(w http.ResponseWriter, r *http.Req
|
||||||
|
|
||||||
// Add tracker version to context
|
// Add tracker version to context
|
||||||
r = r.WithContext(context.WithValue(r.Context(), "tracker", req.TrackerVersion))
|
r = r.WithContext(context.WithValue(r.Context(), "tracker", req.TrackerVersion))
|
||||||
if err := validateTrackerVersion(req.TrackerVersion); err != nil {
|
|
||||||
e.log.Error(r.Context(), "unsupported tracker version: %s, err: %s", req.TrackerVersion, err)
|
|
||||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusUpgradeRequired, errors.New("please upgrade the tracker version"), startTime, r.URL.Path, bodySize)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler's logic
|
// Handler's logic
|
||||||
if req.ProjectKey == nil {
|
if req.ProjectKey == nil {
|
||||||
|
|
@ -162,6 +157,13 @@ func (e *handlersImpl) startSessionHandlerWeb(w http.ResponseWriter, r *http.Req
|
||||||
// Add projectID to context
|
// Add projectID to context
|
||||||
r = r.WithContext(context.WithValue(r.Context(), "projectID", fmt.Sprintf("%d", p.ProjectID)))
|
r = r.WithContext(context.WithValue(r.Context(), "projectID", fmt.Sprintf("%d", p.ProjectID)))
|
||||||
|
|
||||||
|
// Validate tracker version
|
||||||
|
if err := validateTrackerVersion(req.TrackerVersion); err != nil {
|
||||||
|
e.log.Error(r.Context(), "unsupported tracker version: %s, err: %s", req.TrackerVersion, err)
|
||||||
|
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusUpgradeRequired, errors.New("please upgrade the tracker version"), startTime, r.URL.Path, bodySize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the project supports mobile sessions
|
// Check if the project supports mobile sessions
|
||||||
if !p.IsWeb() {
|
if !p.IsWeb() {
|
||||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusForbidden, errors.New("project doesn't support web sessions"), startTime, r.URL.Path, bodySize)
|
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusForbidden, errors.New("project doesn't support web sessions"), startTime, r.URL.Path, bodySize)
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ function ProfilerDoc() {
|
||||||
? sites.find((site) => site.id === siteId)?.projectKey
|
? sites.find((site) => site.id === siteId)?.projectKey
|
||||||
: sites[0]?.projectKey;
|
: sites[0]?.projectKey;
|
||||||
|
|
||||||
const usage = `import OpenReplay from '@openreplay/tracker';
|
const usage = `import { tracker } from '@openreplay/tracker';
|
||||||
import trackerProfiler from '@openreplay/tracker-profiler';
|
import trackerProfiler from '@openreplay/tracker-profiler';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
tracker.start()
|
tracker.start()
|
||||||
|
|
@ -29,10 +29,12 @@ export const profiler = tracker.use(trackerProfiler());
|
||||||
const fn = profiler('call_name')(() => {
|
const fn = profiler('call_name')(() => {
|
||||||
//...
|
//...
|
||||||
}, thisArg); // thisArg is optional`;
|
}, thisArg); // thisArg is optional`;
|
||||||
const usageCjs = `import OpenReplay from '@openreplay/tracker/cjs';
|
const usageCjs = `import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
|
|
||||||
import trackerProfiler from '@openreplay/tracker-profiler/cjs';
|
import trackerProfiler from '@openreplay/tracker-profiler/cjs';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
//...
|
//...
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,19 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function AssistNpm(props) {
|
function AssistNpm(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const usage = `import OpenReplay from '@openreplay/tracker';
|
const usage = `import { tracker } from '@openreplay/tracker';
|
||||||
import trackerAssist from '@openreplay/tracker-assist';
|
import trackerAssist from '@openreplay/tracker-assist';
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${props.projectKey}',
|
projectKey: '${props.projectKey}',
|
||||||
});
|
});
|
||||||
tracker.start()
|
tracker.start()
|
||||||
|
|
||||||
tracker.use(trackerAssist(options)); // check the list of available options below`;
|
tracker.use(trackerAssist(options)); // check the list of available options below`;
|
||||||
const usageCjs = `import OpenReplay from '@openreplay/tracker/cjs';
|
const usageCjs = `import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
import trackerAssist from '@openreplay/tracker-assist/cjs';
|
import trackerAssist from '@openreplay/tracker-assist/cjs';
|
||||||
const tracker = new OpenReplay({
|
|
||||||
|
tracker.configure({
|
||||||
projectKey: '${props.projectKey}'
|
projectKey: '${props.projectKey}'
|
||||||
});
|
});
|
||||||
const trackerAssist = tracker.use(trackerAssist(options)); // check the list of available options below
|
const trackerAssist = tracker.use(trackerAssist(options)); // check the list of available options below
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,20 @@ function GraphQLDoc() {
|
||||||
const projectKey = siteId
|
const projectKey = siteId
|
||||||
? sites.find((site) => site.id === siteId)?.projectKey
|
? sites.find((site) => site.id === siteId)?.projectKey
|
||||||
: sites[0]?.projectKey;
|
: sites[0]?.projectKey;
|
||||||
const usage = `import OpenReplay from '@openreplay/tracker';
|
const usage = `import { tracker } from '@openreplay/tracker';
|
||||||
import trackerGraphQL from '@openreplay/tracker-graphql';
|
import trackerGraphQL from '@openreplay/tracker-graphql';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
tracker.start()
|
tracker.start()
|
||||||
//...
|
//...
|
||||||
export const recordGraphQL = tracker.use(trackerGraphQL());`;
|
export const recordGraphQL = tracker.use(trackerGraphQL());`;
|
||||||
const usageCjs = `import OpenReplay from '@openreplay/tracker/cjs';
|
const usageCjs = `import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
import trackerGraphQL from '@openreplay/tracker-graphql/cjs';
|
import trackerGraphQL from '@openreplay/tracker-graphql/cjs';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
//...
|
//...
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,21 @@ function MobxDoc() {
|
||||||
? sites.find((site) => site.id === siteId)?.projectKey
|
? sites.find((site) => site.id === siteId)?.projectKey
|
||||||
: sites[0]?.projectKey;
|
: sites[0]?.projectKey;
|
||||||
|
|
||||||
const mobxUsage = `import OpenReplay from '@openreplay/tracker';
|
const mobxUsage = `import { tracker } from '@openreplay/tracker';
|
||||||
import trackerMobX from '@openreplay/tracker-mobx';
|
import trackerMobX from '@openreplay/tracker-mobx';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
tracker.use(trackerMobX(<options>)); // check list of available options below
|
tracker.use(trackerMobX(<options>)); // check list of available options below
|
||||||
tracker.start();
|
tracker.start();
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const mobxUsageCjs = `import OpenReplay from '@openreplay/tracker/cjs';
|
const mobxUsageCjs = `import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
import trackerMobX from '@openreplay/tracker-mobx/cjs';
|
import trackerMobX from '@openreplay/tracker-mobx/cjs';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
tracker.use(trackerMobX(<options>)); // check list of available options below
|
tracker.use(trackerMobX(<options>)); // check list of available options below
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ function NgRxDoc() {
|
||||||
: sites[0]?.projectKey;
|
: sites[0]?.projectKey;
|
||||||
const usage = `import { StoreModule } from '@ngrx/store';
|
const usage = `import { StoreModule } from '@ngrx/store';
|
||||||
import { reducers } from './reducers';
|
import { reducers } from './reducers';
|
||||||
import OpenReplay from '@openreplay/tracker';
|
import { tracker } from '@openreplay/tracker';
|
||||||
import trackerNgRx from '@openreplay/tracker-ngrx';
|
import trackerNgRx from '@openreplay/tracker-ngrx';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
tracker.start()
|
tracker.start()
|
||||||
|
|
@ -32,10 +32,11 @@ const metaReducers = [tracker.use(trackerNgRx(<options>))]; // check list of ava
|
||||||
export class AppModule {}`;
|
export class AppModule {}`;
|
||||||
const usageCjs = `import { StoreModule } from '@ngrx/store';
|
const usageCjs = `import { StoreModule } from '@ngrx/store';
|
||||||
import { reducers } from './reducers';
|
import { reducers } from './reducers';
|
||||||
import OpenReplay from '@openreplay/tracker/cjs';
|
import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
import trackerNgRx from '@openreplay/tracker-ngrx/cjs';
|
import trackerNgRx from '@openreplay/tracker-ngrx/cjs';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
//...
|
//...
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ function PiniaDoc() {
|
||||||
? sites.find((site) => site.id === siteId)?.projectKey
|
? sites.find((site) => site.id === siteId)?.projectKey
|
||||||
: sites[0]?.projectKey;
|
: sites[0]?.projectKey;
|
||||||
const usage = `import Vuex from 'vuex'
|
const usage = `import Vuex from 'vuex'
|
||||||
import OpenReplay from '@openreplay/tracker';
|
import { tracker } from '@openreplay/tracker';
|
||||||
import trackerVuex from '@openreplay/tracker-vuex';
|
import trackerVuex from '@openreplay/tracker-vuex';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
tracker.start()
|
tracker.start()
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ function ReduxDoc() {
|
||||||
: sites[0]?.projectKey;
|
: sites[0]?.projectKey;
|
||||||
|
|
||||||
const usage = `import { applyMiddleware, createStore } from 'redux';
|
const usage = `import { applyMiddleware, createStore } from 'redux';
|
||||||
import OpenReplay from '@openreplay/tracker';
|
import { tracker } from '@openreplay/tracker';
|
||||||
import trackerRedux from '@openreplay/tracker-redux';
|
import trackerRedux from '@openreplay/tracker-redux';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
tracker.start()
|
tracker.start()
|
||||||
|
|
@ -29,10 +29,11 @@ const store = createStore(
|
||||||
applyMiddleware(tracker.use(trackerRedux(<options>))) // check list of available options below
|
applyMiddleware(tracker.use(trackerRedux(<options>))) // check list of available options below
|
||||||
);`;
|
);`;
|
||||||
const usageCjs = `import { applyMiddleware, createStore } from 'redux';
|
const usageCjs = `import { applyMiddleware, createStore } from 'redux';
|
||||||
import OpenReplay from '@openreplay/tracker/cjs';
|
import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
import trackerRedux from '@openreplay/tracker-redux/cjs';
|
import trackerRedux from '@openreplay/tracker-redux/cjs';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
//...
|
//...
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ function VueDoc() {
|
||||||
: sites[0]?.projectKey;
|
: sites[0]?.projectKey;
|
||||||
|
|
||||||
const usage = `import Vuex from 'vuex'
|
const usage = `import Vuex from 'vuex'
|
||||||
import OpenReplay from '@openreplay/tracker';
|
import { tracker } from '@openreplay/tracker';
|
||||||
import trackerVuex from '@openreplay/tracker-vuex';
|
import trackerVuex from '@openreplay/tracker-vuex';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
tracker.start()
|
tracker.start()
|
||||||
|
|
@ -29,10 +29,11 @@ const store = new Vuex.Store({
|
||||||
plugins: [tracker.use(trackerVuex(<options>))] // check list of available options below
|
plugins: [tracker.use(trackerVuex(<options>))] // check list of available options below
|
||||||
});`;
|
});`;
|
||||||
const usageCjs = `import Vuex from 'vuex'
|
const usageCjs = `import Vuex from 'vuex'
|
||||||
import OpenReplay from '@openreplay/tracker/cjs';
|
import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
import trackerVuex from '@openreplay/tracker-vuex/cjs';
|
import trackerVuex from '@openreplay/tracker-vuex/cjs';
|
||||||
//...
|
//...
|
||||||
const tracker = new OpenReplay({
|
tracker.configure({
|
||||||
projectKey: '${projectKey}'
|
projectKey: '${projectKey}'
|
||||||
});
|
});
|
||||||
//...
|
//...
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,10 @@ function ZustandDoc(props) {
|
||||||
: sites[0]?.projectKey;
|
: sites[0]?.projectKey;
|
||||||
|
|
||||||
const usage = `import create from "zustand";
|
const usage = `import create from "zustand";
|
||||||
import Tracker from '@openreplay/tracker';
|
import { tracker } from '@openreplay/tracker';
|
||||||
import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand';
|
import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand';
|
||||||
|
|
||||||
|
tracker.configure({
|
||||||
const tracker = new Tracker({
|
|
||||||
projectKey: ${projectKey},
|
projectKey: ${projectKey},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -43,11 +42,12 @@ const useBearStore = create(
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
const usageCjs = `import create from "zustand";
|
const usageCjs = `import create from "zustand";
|
||||||
import Tracker from '@openreplay/tracker/cjs';
|
import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand/cjs';
|
import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand/cjs';
|
||||||
|
|
||||||
|
|
||||||
const tracker = new Tracker({
|
tracker.configure({
|
||||||
projectKey: ${projectKey},
|
projectKey: ${projectKey},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,17 @@ import stl from './installDocs.module.css';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const installationCommand = 'npm i @openreplay/tracker';
|
const installationCommand = 'npm i @openreplay/tracker';
|
||||||
const usageCode = `import Tracker from '@openreplay/tracker';
|
const usageCode = `import { tracker } from '@openreplay/tracker';
|
||||||
|
|
||||||
const tracker = new Tracker({
|
tracker.configure({
|
||||||
projectKey: "PROJECT_KEY",
|
projectKey: "PROJECT_KEY",
|
||||||
ingestPoint: "https://${window.location.hostname}/ingest",
|
ingestPoint: "https://${window.location.hostname}/ingest",
|
||||||
});
|
});
|
||||||
tracker.start()`;
|
tracker.start()`;
|
||||||
const usageCodeSST = `import Tracker from '@openreplay/tracker/cjs';
|
const usageCodeSST = `import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
|
|
||||||
const tracker = new Tracker({
|
tracker.configure({
|
||||||
projectKey: "PROJECT_KEY",
|
projectKey: "PROJECT_KEY",
|
||||||
ingestPoint: "https://${window.location.hostname}/ingest",
|
ingestPoint: "https://${window.location.hostname}/ingest",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,18 @@ import stl from './installDocs.module.css';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const installationCommand = 'npm i @openreplay/tracker';
|
const installationCommand = 'npm i @openreplay/tracker';
|
||||||
const usageCode = `import Tracker from '@openreplay/tracker';
|
const usageCode = `import { tracker } from '@openreplay/tracker';
|
||||||
|
|
||||||
const tracker = new Tracker({
|
tracker.configure({
|
||||||
projectKey: "PROJECT_KEY",
|
projectKey: "PROJECT_KEY",
|
||||||
ingestPoint: "https://${window.location.hostname}/ingest",
|
ingestPoint: "https://${window.location.hostname}/ingest",
|
||||||
});
|
});
|
||||||
|
|
||||||
tracker.start()`;
|
tracker.start()`;
|
||||||
const usageCodeSST = `import Tracker from '@openreplay/tracker/cjs';
|
const usageCodeSST = `import { tracker } from '@openreplay/tracker/cjs';
|
||||||
|
// alternatively you can use dynamic import without /cjs suffix to prevent issues with window scope
|
||||||
|
|
||||||
const tracker = new Tracker({
|
tracker.configure({
|
||||||
projectKey: "PROJECT_KEY",
|
projectKey: "PROJECT_KEY",
|
||||||
ingestPoint: "https://${window.location.hostname}/ingest",
|
ingestPoint: "https://${window.location.hostname}/ingest",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,18 +28,18 @@ export const checkValues = (key: any, value: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterMap = ({
|
export const filterMap = ({
|
||||||
category,
|
category,
|
||||||
value,
|
value,
|
||||||
key,
|
key,
|
||||||
operator,
|
operator,
|
||||||
sourceOperator,
|
sourceOperator,
|
||||||
source,
|
source,
|
||||||
custom,
|
custom,
|
||||||
isEvent,
|
isEvent,
|
||||||
filters,
|
filters,
|
||||||
sort,
|
sort,
|
||||||
order
|
order
|
||||||
}: any) => ({
|
}: any) => ({
|
||||||
value: checkValues(key, value),
|
value: checkValues(key, value),
|
||||||
custom,
|
custom,
|
||||||
type: category === FilterCategory.METADATA ? FilterKey.METADATA : key,
|
type: category === FilterCategory.METADATA ? FilterKey.METADATA : key,
|
||||||
|
|
@ -254,7 +254,7 @@ class SearchStore {
|
||||||
|
|
||||||
this.savedSearch = new SavedSearch({});
|
this.savedSearch = new SavedSearch({});
|
||||||
sessionStore.clearList();
|
sessionStore.clearList();
|
||||||
void this.fetchSessions(true);
|
// void this.fetchSessions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkForLatestSessionCount(): Promise<void> {
|
async checkForLatestSessionCount(): Promise<void> {
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,3 +1,7 @@
|
||||||
|
## 16.0.2
|
||||||
|
|
||||||
|
- fix attributeSender key generation to prevent calling native methods on objects
|
||||||
|
|
||||||
## 16.0.1
|
## 16.0.1
|
||||||
|
|
||||||
- drop computing ts digits
|
- drop computing ts digits
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@openreplay/tracker",
|
"name": "@openreplay/tracker",
|
||||||
"description": "The OpenReplay tracker main package",
|
"description": "The OpenReplay tracker main package",
|
||||||
"version": "16.0.1",
|
"version": "16.0.2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"logging",
|
"logging",
|
||||||
"replay"
|
"replay"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import TrackerClass from './index.js'
|
import TrackerClass from './index'
|
||||||
|
|
||||||
export { default as App } from './app/index.js'
|
export { default as App } from './app/index'
|
||||||
export { SanitizeLevel, Messages, Options } from './index.js'
|
export { default as tracker, default as openReplay } from './singleton'
|
||||||
export { default as tracker } from './singleton.js'
|
export { SanitizeLevel, Messages, Options } from './index'
|
||||||
|
export { default as Analytics } from './modules/analytics/index'
|
||||||
export default TrackerClass
|
export default TrackerClass
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import App from './app/index.js'
|
import App from './app/index'
|
||||||
|
|
||||||
export { default as App } from './app/index.js'
|
export { default as App } from './app/index.js'
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ import Network from './modules/network.js'
|
||||||
import ConstructedStyleSheets from './modules/constructedStyleSheets.js'
|
import ConstructedStyleSheets from './modules/constructedStyleSheets.js'
|
||||||
import Selection from './modules/selection.js'
|
import Selection from './modules/selection.js'
|
||||||
import Tabs from './modules/tabs.js'
|
import Tabs from './modules/tabs.js'
|
||||||
|
import AnalyticsSDK from './modules/analytics/index.js'
|
||||||
|
|
||||||
import { IN_BROWSER, deprecationWarn, DOCS_HOST, inIframe } from './utils.js'
|
import { IN_BROWSER, deprecationWarn, DOCS_HOST, inIframe } from './utils.js'
|
||||||
import FeatureFlags, { IFeatureFlag } from './modules/featureFlags.js'
|
import FeatureFlags, { IFeatureFlag } from './modules/featureFlags.js'
|
||||||
|
|
@ -107,6 +108,7 @@ export default class API {
|
||||||
public featureFlags: FeatureFlags
|
public featureFlags: FeatureFlags
|
||||||
|
|
||||||
private readonly app: App | null = null
|
private readonly app: App | null = null
|
||||||
|
public readonly analytics: AnalyticsSDK | null = null
|
||||||
private readonly crossdomainMode: boolean = false
|
private readonly crossdomainMode: boolean = false
|
||||||
|
|
||||||
constructor(public readonly options: Partial<Options>) {
|
constructor(public readonly options: Partial<Options>) {
|
||||||
|
|
@ -178,6 +180,13 @@ export default class API {
|
||||||
this.signalStartIssue,
|
this.signalStartIssue,
|
||||||
this.crossdomainMode,
|
this.crossdomainMode,
|
||||||
)
|
)
|
||||||
|
this.analytics = new AnalyticsSDK(
|
||||||
|
options.localStorage ?? localStorage,
|
||||||
|
options.sessionStorage ?? sessionStorage,
|
||||||
|
this.getAnalyticsToken,
|
||||||
|
this.app?.timestamp ?? Date.now,
|
||||||
|
this.setUserID,
|
||||||
|
)
|
||||||
this.app = app
|
this.app = app
|
||||||
if (!this.crossdomainMode) {
|
if (!this.crossdomainMode) {
|
||||||
// no need to send iframe viewport data since its a node for us
|
// no need to send iframe viewport data since its a node for us
|
||||||
|
|
@ -317,6 +326,9 @@ export default class API {
|
||||||
if (this.app === null) {
|
if (this.app === null) {
|
||||||
return Promise.reject("Browser doesn't support required api, or doNotTrack is active.")
|
return Promise.reject("Browser doesn't support required api, or doNotTrack is active.")
|
||||||
}
|
}
|
||||||
|
if (startOpts?.userID) {
|
||||||
|
this.analytics?.people.identify(startOpts.userID, { fromTracker: true })
|
||||||
|
}
|
||||||
return this.app.start(startOpts)
|
return this.app.start(startOpts)
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject('Trying to start not in browser.')
|
return Promise.reject('Trying to start not in browser.')
|
||||||
|
|
@ -452,6 +464,7 @@ export default class API {
|
||||||
setUserID(id: string): void {
|
setUserID(id: string): void {
|
||||||
if (typeof id === 'string' && this.app !== null) {
|
if (typeof id === 'string' && this.app !== null) {
|
||||||
this.app.session.setUserID(id)
|
this.app.session.setUserID(id)
|
||||||
|
this.analytics?.people.identify(id, { fromTracker: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -528,4 +541,19 @@ export default class API {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private analyticsToken: string | null = null
|
||||||
|
/**
|
||||||
|
* Use custom token for analytics events without session recording
|
||||||
|
* */
|
||||||
|
public setAnalyticsToken = (token: string) => {
|
||||||
|
this.analyticsToken = token
|
||||||
|
}
|
||||||
|
public getAnalyticsToken = () => {
|
||||||
|
if (this.analyticsToken) {
|
||||||
|
return this.analyticsToken
|
||||||
|
} else {
|
||||||
|
return this.app?.session.getSessionToken() ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
136
tracker/tracker/src/main/modules/analytics/events.ts
Normal file
136
tracker/tracker/src/main/modules/analytics/events.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import SharedProperties from './sharedProperties.js'
|
||||||
|
import { isObject } from './utils.js'
|
||||||
|
|
||||||
|
const maxProperties = 100;
|
||||||
|
const maxPropLength = 100;
|
||||||
|
|
||||||
|
export default class Events {
|
||||||
|
queue: Record<string, any> = []
|
||||||
|
sendInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
ownProperties: Record<string, any> = {}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly sharedProperties: SharedProperties,
|
||||||
|
private readonly getToken: () => string,
|
||||||
|
private readonly getTimestamp: () => number,
|
||||||
|
private readonly batchInterval = 5000,
|
||||||
|
) {
|
||||||
|
this.sendInterval = setInterval(() => {
|
||||||
|
void this.sendBatch()
|
||||||
|
}, batchInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event to batch with option to send it immediately,
|
||||||
|
* properties are optional and will not be saved as super prop
|
||||||
|
* */
|
||||||
|
sendEvent = (
|
||||||
|
eventName: string,
|
||||||
|
properties?: Record<string, any>,
|
||||||
|
options?: { send_immediately: boolean },
|
||||||
|
) => {
|
||||||
|
// add properties
|
||||||
|
const eventProps = {}
|
||||||
|
if (properties) {
|
||||||
|
if (!isObject(properties)) {
|
||||||
|
throw new Error('Properties must be an object')
|
||||||
|
}
|
||||||
|
Object.entries(properties).forEach(([key, value]) => {
|
||||||
|
if (!this.sharedProperties.defaultPropertyKeys.includes(key)) {
|
||||||
|
eventProps[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const event = {
|
||||||
|
event: eventName,
|
||||||
|
properties: { ...this.sharedProperties.all, ...this.ownProperties, ...eventProps },
|
||||||
|
timestamp: this.getTimestamp(),
|
||||||
|
}
|
||||||
|
if (options?.send_immediately) {
|
||||||
|
void this.sendSingle(event)
|
||||||
|
} else {
|
||||||
|
this.queue.push(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendBatch = async () => {
|
||||||
|
if (this.queue.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${this.getToken()}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
// await fetch blah blah + token
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSingle = async (event) => {
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${this.getToken()}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
// await fetch blah blah + token
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates super property for all events
|
||||||
|
* TODO: export as tracker.register
|
||||||
|
* */
|
||||||
|
setProperty = (nameOrProperties: Record<string, any> | string, value?: any) => {
|
||||||
|
if (isObject(nameOrProperties)) {
|
||||||
|
Object.entries(nameOrProperties).forEach(([key, val]) => {
|
||||||
|
if (!this.sharedProperties.defaultPropertyKeys.includes(key)) {
|
||||||
|
this.ownProperties[key] = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (typeof nameOrProperties === 'string' && value !== undefined) {
|
||||||
|
if (!this.sharedProperties.defaultPropertyKeys.includes(nameOrProperties)) {
|
||||||
|
this.ownProperties[nameOrProperties] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set super property only if it doesn't exist
|
||||||
|
* TODO: export as register_once
|
||||||
|
* */
|
||||||
|
setPropertiesOnce = (nameOrProperties: Record<string, any> | string, value?: any) => {
|
||||||
|
if (isObject(nameOrProperties)) {
|
||||||
|
Object.entries(nameOrProperties).forEach(([key, val]) => {
|
||||||
|
if (!this.ownProperties[key] && !reservedProps.includes(key)) {
|
||||||
|
this.ownProperties[key] = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (typeof nameOrProperties === 'string' && value !== undefined) {
|
||||||
|
if (!this.ownProperties[nameOrProperties] && !reservedProps.includes(nameOrProperties)) {
|
||||||
|
this.ownProperties[nameOrProperties] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* removes properties from list
|
||||||
|
* TODO: export as unregister
|
||||||
|
* */
|
||||||
|
unsetProperties = (properties: string | string[]) => {
|
||||||
|
if (Array.isArray(properties)) {
|
||||||
|
properties.forEach((key) => {
|
||||||
|
if (this.ownProperties[key] && !reservedProps.includes(key)) {
|
||||||
|
delete this.ownProperties[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (this.ownProperties[properties] && !reservedProps.includes(properties)) {
|
||||||
|
delete this.ownProperties[properties]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDynamicProperties = () => {
|
||||||
|
return {
|
||||||
|
$auto_captured: false,
|
||||||
|
$current_url: window.location.href,
|
||||||
|
$referrer: document.referrer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
tracker/tracker/src/main/modules/analytics/index.ts
Normal file
35
tracker/tracker/src/main/modules/analytics/index.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import SharedProperties from './sharedProperties.js'
|
||||||
|
import type { StorageLike } from './sharedProperties.js'
|
||||||
|
import Events from './events.js'
|
||||||
|
import People from './people.js'
|
||||||
|
|
||||||
|
export default class Analytics {
|
||||||
|
public readonly events: Events
|
||||||
|
public readonly sharedProperties: SharedProperties
|
||||||
|
public readonly people: People
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param localStorage Class or Object that implements Storage-like interface that stores
|
||||||
|
* values persistently like window.localStorage or any other file-based storage
|
||||||
|
*
|
||||||
|
* @param sessionStorage Class or Object that implements Storage-like interface that stores values
|
||||||
|
* on per-session basis like window.sessionStorage or any other in-memory storage
|
||||||
|
*
|
||||||
|
* @param getToken Function that returns token to bind events to a session
|
||||||
|
*
|
||||||
|
* @param getTimestamp returns current timestamp
|
||||||
|
*
|
||||||
|
* @param setUserId callback for people.identify
|
||||||
|
* */
|
||||||
|
constructor(
|
||||||
|
private readonly localStorage: StorageLike,
|
||||||
|
private readonly sessionStorage: StorageLike,
|
||||||
|
private readonly getToken: () => string,
|
||||||
|
private readonly getTimestamp: () => number,
|
||||||
|
private readonly setUserId: (user_id: string) => void,
|
||||||
|
) {
|
||||||
|
this.sharedProperties = new SharedProperties(localStorage, sessionStorage)
|
||||||
|
this.events = new Events(this.sharedProperties, getToken, getTimestamp)
|
||||||
|
this.people = new People(this.sharedProperties, getToken, getTimestamp, setUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
130
tracker/tracker/src/main/modules/analytics/people.ts
Normal file
130
tracker/tracker/src/main/modules/analytics/people.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import SharedProperties from './sharedProperties.js'
|
||||||
|
import { isObject } from './utils.js'
|
||||||
|
|
||||||
|
export default class People {
|
||||||
|
ownProperties: Record<string, any> = {}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly sharedProperties: SharedProperties,
|
||||||
|
private readonly getToken: () => string,
|
||||||
|
private readonly getTimestamp: () => number,
|
||||||
|
private readonly onId: (user_id: string) => void,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
identify = (user_id: string, options?: { fromTracker: boolean }) => {
|
||||||
|
this.sharedProperties.setUserId(user_id)
|
||||||
|
if (!options?.fromTracker) {
|
||||||
|
this.onId(user_id)
|
||||||
|
}
|
||||||
|
// TODO: fetch endpoint when it will be here
|
||||||
|
}
|
||||||
|
|
||||||
|
// add "hard" flag to force generate device id as well ?
|
||||||
|
reset = () => {
|
||||||
|
this.sharedProperties.resetUserId()
|
||||||
|
this.ownProperties = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
get user_id() {
|
||||||
|
return this.sharedProperties.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: what exactly we're removing here besides properties and id?
|
||||||
|
deleteUser = () => {
|
||||||
|
this.sharedProperties.setUserId(null)
|
||||||
|
this.ownProperties = {}
|
||||||
|
|
||||||
|
// TODO: fetch endpoint when it will be here
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set ownProperties, overwriting entire object
|
||||||
|
*
|
||||||
|
* TODO: exported as people.set
|
||||||
|
* */
|
||||||
|
setProperties = (properties: Record<string, any>) => {
|
||||||
|
if (!isObject(properties)) {
|
||||||
|
throw new Error('Properties must be an object')
|
||||||
|
}
|
||||||
|
Object.entries(properties).forEach(([key, value]) => {
|
||||||
|
if (!this.sharedProperties.defaultPropertyKeys.includes(key)) {
|
||||||
|
this.ownProperties[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set property if it doesn't exist yet
|
||||||
|
*
|
||||||
|
* TODO: exported as people.set_once
|
||||||
|
* */
|
||||||
|
setPropertiesOnce = (properties: Record<string, any>) => {
|
||||||
|
if (!isObject(properties)) {
|
||||||
|
throw new Error('Properties must be an object')
|
||||||
|
}
|
||||||
|
Object.entries(properties).forEach(([key, value]) => {
|
||||||
|
if (!this.sharedProperties.defaultPropertyKeys.includes(key) && !this.ownProperties[key]) {
|
||||||
|
this.ownProperties[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add value to property (will turn string prop into array)
|
||||||
|
*
|
||||||
|
* TODO: exported as people.append
|
||||||
|
* */
|
||||||
|
appendValues = (key: string, value: string | number) => {
|
||||||
|
if (!this.sharedProperties.defaultPropertyKeys.includes(key) && this.ownProperties[key]) {
|
||||||
|
if (Array.isArray(this.ownProperties[key])) {
|
||||||
|
this.ownProperties[key].push(value)
|
||||||
|
} else {
|
||||||
|
this.ownProperties[key] = [this.ownProperties[key], value]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add unique values to property (will turn string prop into array)
|
||||||
|
*
|
||||||
|
* TODO: exported as people.union
|
||||||
|
* */
|
||||||
|
appendUniqueValues = (key: string, value: string | number) => {
|
||||||
|
if (!this.ownProperties[key]) return
|
||||||
|
if (Array.isArray(this.ownProperties[key])) {
|
||||||
|
if (!this.ownProperties[key].includes(value)) {
|
||||||
|
this.appendValues(key, value)
|
||||||
|
}
|
||||||
|
} else if (this.ownProperties[key] !== value) {
|
||||||
|
this.appendValues(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds value (incl. negative) to existing numerical property
|
||||||
|
*
|
||||||
|
* TODO: exported as people.increment
|
||||||
|
* */
|
||||||
|
increment = (key: string, value: number) => {
|
||||||
|
if (!this.sharedProperties.defaultPropertyKeys.includes(key) && typeof this.ownProperties[key] === 'number') {
|
||||||
|
this.ownProperties[key] += value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUserProperties = async () => {
|
||||||
|
if (!this.user_id) return
|
||||||
|
const userObj = {
|
||||||
|
user_id: this.user_id,
|
||||||
|
distinct_id: this.sharedProperties.distinctId,
|
||||||
|
properties: {
|
||||||
|
...this.sharedProperties.all,
|
||||||
|
...this.ownProperties,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${this.getToken()}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch user properties
|
||||||
|
}
|
||||||
|
}
|
||||||
144
tracker/tracker/src/main/modules/analytics/sharedProperties.ts
Normal file
144
tracker/tracker/src/main/modules/analytics/sharedProperties.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { getUTCOffsetString, uaParse } from './utils.js'
|
||||||
|
|
||||||
|
export interface StorageLike {
|
||||||
|
getItem: (key: string) => string | null
|
||||||
|
setItem: (key: string, value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const refKey = '$__initial_ref__$'
|
||||||
|
const distinctIdKey = '$__distinct_device_id__$'
|
||||||
|
const prefix = '$'
|
||||||
|
|
||||||
|
const searchEngineList = [
|
||||||
|
'google',
|
||||||
|
'bing',
|
||||||
|
'yahoo',
|
||||||
|
'baidu',
|
||||||
|
'yandex',
|
||||||
|
'duckduckgo',
|
||||||
|
'ecosia',
|
||||||
|
'ask',
|
||||||
|
'aol',
|
||||||
|
'wolframalpha',
|
||||||
|
'startpage',
|
||||||
|
'swisscows',
|
||||||
|
'qwant',
|
||||||
|
'lycos',
|
||||||
|
'dogpile',
|
||||||
|
'info',
|
||||||
|
'teoma',
|
||||||
|
'webcrawler',
|
||||||
|
'naver',
|
||||||
|
'seznam',
|
||||||
|
'perplexity',
|
||||||
|
]
|
||||||
|
|
||||||
|
export default class SharedProperties {
|
||||||
|
os: string
|
||||||
|
osVersion: string
|
||||||
|
browser: string
|
||||||
|
browserVersion: string
|
||||||
|
device: string
|
||||||
|
screenHeight: number
|
||||||
|
screenWidth: number
|
||||||
|
initialReferrer: string
|
||||||
|
utmSource: string | null
|
||||||
|
utmMedium: string | null
|
||||||
|
utmCampaign: string | null
|
||||||
|
deviceId: string
|
||||||
|
searchEngine: string | null
|
||||||
|
user_id: string | null = null
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly localStorage: StorageLike,
|
||||||
|
private readonly sessionStorage: StorageLike,
|
||||||
|
) {
|
||||||
|
const { width, height, browser, browserVersion, browserMajorVersion, os, osVersion, mobile } =
|
||||||
|
uaParse(window)
|
||||||
|
this.os = os
|
||||||
|
this.osVersion = osVersion
|
||||||
|
this.browser = `${browser}`
|
||||||
|
this.browserVersion = `${browserVersion} (${browserMajorVersion})`
|
||||||
|
this.device = mobile ? 'Mobile' : 'Desktop'
|
||||||
|
this.screenHeight = height
|
||||||
|
this.screenWidth = width
|
||||||
|
this.initialReferrer = this.getReferrer()
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
this.utmSource = searchParams.get('utm_source') || null
|
||||||
|
this.utmMedium = searchParams.get('utm_medium') || null
|
||||||
|
this.utmCampaign = searchParams.get('utm_campaign') || null
|
||||||
|
this.deviceId = this.getDistinctDeviceId()
|
||||||
|
this.searchEngine = this.getSerachEngine(this.initialReferrer)
|
||||||
|
}
|
||||||
|
|
||||||
|
public get all() {
|
||||||
|
return {
|
||||||
|
[`${prefix}os`]: this.os,
|
||||||
|
[`${prefix}os_version`]: this.osVersion,
|
||||||
|
[`${prefix}browser`]: this.browser,
|
||||||
|
[`${prefix}browser_version`]: this.browserVersion,
|
||||||
|
[`${prefix}device`]: this.device,
|
||||||
|
[`${prefix}screen_height`]: this.screenHeight,
|
||||||
|
[`${prefix}screen_width`]: this.screenWidth,
|
||||||
|
[`${prefix}initial_referrer`]: this.initialReferrer,
|
||||||
|
[`${prefix}utm_source`]: this.utmSource,
|
||||||
|
[`${prefix}utm_medium`]: this.utmMedium,
|
||||||
|
[`${prefix}utm_campaign`]: this.utmCampaign,
|
||||||
|
[`${prefix}device_id`]: this.deviceId,
|
||||||
|
[`${prefix}user_id`]: this.user_id,
|
||||||
|
[`${prefix}distinct_id`]: this.user_id ?? this.deviceId,
|
||||||
|
[`${prefix}sdk_edition`]: 'web',
|
||||||
|
[`${prefix}sdk_version`]: 'TRACKER_VERSION',
|
||||||
|
[`${prefix}timezone`]: getUTCOffsetString(),
|
||||||
|
[`${prefix}search_engine`]: this.searchEngine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserId = (user_id: string | null) => {
|
||||||
|
this.user_id = user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
resetUserId = () => {
|
||||||
|
this.user_id = null
|
||||||
|
this.deviceId = this.getDistinctDeviceId(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public get defaultPropertyKeys() {
|
||||||
|
return Object.keys(this.all)
|
||||||
|
}
|
||||||
|
|
||||||
|
public get distinctId() {
|
||||||
|
return this.deviceId
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDistinctDeviceId = (force?: boolean) => {
|
||||||
|
const potentialStored = this.localStorage.getItem(distinctIdKey)
|
||||||
|
if (potentialStored && !force) {
|
||||||
|
return potentialStored
|
||||||
|
} else {
|
||||||
|
const distinctId = `${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`
|
||||||
|
this.localStorage.setItem(distinctIdKey, distinctId)
|
||||||
|
return distinctId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getReferrer = () => {
|
||||||
|
const potentialStored = this.sessionStorage.getItem(refKey)
|
||||||
|
if (potentialStored) {
|
||||||
|
return potentialStored
|
||||||
|
} else {
|
||||||
|
const ref = document.referrer
|
||||||
|
this.sessionStorage.setItem(refKey, ref)
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSerachEngine = (ref: string) => {
|
||||||
|
for (const searchEngine of searchEngineList) {
|
||||||
|
if (ref.includes(searchEngine)) {
|
||||||
|
return searchEngine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
201
tracker/tracker/src/main/modules/analytics/tests/events.test.ts
Normal file
201
tracker/tracker/src/main/modules/analytics/tests/events.test.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'
|
||||||
|
import Events from '../events.js'
|
||||||
|
|
||||||
|
describe('Events', () => {
|
||||||
|
let mockSharedProperties
|
||||||
|
let events
|
||||||
|
let mockTimestamp
|
||||||
|
let mockToken
|
||||||
|
let mockGetTimestamp
|
||||||
|
let originalSetInterval
|
||||||
|
let originalClearInterval
|
||||||
|
let setIntervalMock
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalSetInterval = global.setInterval
|
||||||
|
originalClearInterval = global.clearInterval
|
||||||
|
|
||||||
|
setIntervalMock = jest.fn(() => 123)
|
||||||
|
global.setInterval = setIntervalMock
|
||||||
|
global.clearInterval = jest.fn()
|
||||||
|
|
||||||
|
mockTimestamp = 1635186000000 // Example timestamp
|
||||||
|
mockGetTimestamp = jest.fn(() => mockTimestamp)
|
||||||
|
|
||||||
|
mockSharedProperties = {
|
||||||
|
all: {
|
||||||
|
$__os: 'Windows 10',
|
||||||
|
$__browser: 'Chrome 91.0.4472.124 (91)',
|
||||||
|
$__device: 'Desktop',
|
||||||
|
$__screenHeight: 1080,
|
||||||
|
$__screenWidth: 1920,
|
||||||
|
$__initialReferrer: 'https://example.com',
|
||||||
|
$__utmSource: 'test_source',
|
||||||
|
$__utmMedium: 'test_medium',
|
||||||
|
$__utmCampaign: 'test_campaign',
|
||||||
|
$__deviceId: 'test-device-id',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockToken = 'test-token-123'
|
||||||
|
|
||||||
|
events = new Events(mockSharedProperties, mockToken, mockGetTimestamp, 1000)
|
||||||
|
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ success: true }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.setInterval = originalSetInterval
|
||||||
|
global.clearInterval = originalClearInterval
|
||||||
|
|
||||||
|
if (events.sendInterval) {
|
||||||
|
clearInterval(events.sendInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('constructor sets up event queue and batch interval', () => {
|
||||||
|
expect(events.queue).toEqual([])
|
||||||
|
expect(events.ownProperties).toEqual({})
|
||||||
|
expect(setIntervalMock).toHaveBeenCalledWith(expect.any(Function), 1000)
|
||||||
|
expect(events.sendInterval).toBe(123)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sendEvent adds event to queue with correct properties', () => {
|
||||||
|
events.sendEvent('test_event', { testProp: 'value' })
|
||||||
|
|
||||||
|
expect(events.queue).toHaveLength(1)
|
||||||
|
expect(events.queue[0]).toEqual({
|
||||||
|
event: 'test_event',
|
||||||
|
properties: {
|
||||||
|
...mockSharedProperties.all,
|
||||||
|
testProp: 'value',
|
||||||
|
},
|
||||||
|
timestamp: mockTimestamp,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sendEvent validates properties are objects', () => {
|
||||||
|
expect(() => {
|
||||||
|
events.sendEvent('test_event', 'not an object')
|
||||||
|
}).toThrow('Properties must be an object')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sendEvent with send_immediately option calls sendSingle', async () => {
|
||||||
|
const sendSingleSpy = jest.spyOn(events, 'sendSingle').mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
await events.sendEvent('immediate_test', { testProp: 'value' }, { send_immediately: true })
|
||||||
|
|
||||||
|
expect(sendSingleSpy).toHaveBeenCalledWith({
|
||||||
|
event: 'immediate_test',
|
||||||
|
properties: {
|
||||||
|
...mockSharedProperties.all,
|
||||||
|
testProp: 'value',
|
||||||
|
},
|
||||||
|
timestamp: mockTimestamp,
|
||||||
|
})
|
||||||
|
expect(events.queue).toHaveLength(0) // Should not add to queue
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sendBatch does nothing when queue is empty', async () => {
|
||||||
|
await events.sendBatch()
|
||||||
|
expect(global.fetch).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setProperty sets a single property correctly', () => {
|
||||||
|
events.setProperty('testProp', 'value')
|
||||||
|
expect(events.ownProperties).toEqual({ testProp: 'value' })
|
||||||
|
|
||||||
|
events.setProperty('event_name', 'should not be set')
|
||||||
|
expect(events.ownProperties.event_name).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setProperty sets multiple properties from object', () => {
|
||||||
|
events.setProperty({
|
||||||
|
prop1: 'value1',
|
||||||
|
prop2: 'value2',
|
||||||
|
event_name: 'should not be set', // Reserved
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(events.ownProperties).toEqual({
|
||||||
|
prop1: 'value1',
|
||||||
|
prop2: 'value2',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setPropertiesOnce only sets properties that do not exist', () => {
|
||||||
|
events.setProperty('existingProp', 'initial value')
|
||||||
|
|
||||||
|
events.setPropertiesOnce({
|
||||||
|
existingProp: 'new value',
|
||||||
|
newProp: 'value',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(events.ownProperties).toEqual({
|
||||||
|
existingProp: 'initial value', // Should not change
|
||||||
|
newProp: 'value', // Should be added
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setPropertiesOnce sets a single property if it does not exist', () => {
|
||||||
|
events.setPropertiesOnce('newProp', 'value')
|
||||||
|
expect(events.ownProperties).toEqual({ newProp: 'value' })
|
||||||
|
|
||||||
|
events.setPropertiesOnce('newProp', 'new value')
|
||||||
|
expect(events.ownProperties).toEqual({ newProp: 'value' }) // Should not change
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unsetProperties removes a single property', () => {
|
||||||
|
events.setProperty({
|
||||||
|
prop1: 'value1',
|
||||||
|
prop2: 'value2',
|
||||||
|
})
|
||||||
|
events.unsetProperties('prop1')
|
||||||
|
|
||||||
|
expect(events.ownProperties).toEqual({
|
||||||
|
prop2: 'value2',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unsetProperties removes multiple properties', () => {
|
||||||
|
events.setProperty({
|
||||||
|
prop1: 'value1',
|
||||||
|
prop2: 'value2',
|
||||||
|
prop3: 'value3',
|
||||||
|
})
|
||||||
|
events.unsetProperties(['prop1', 'prop3'])
|
||||||
|
|
||||||
|
expect(events.ownProperties).toEqual({
|
||||||
|
prop2: 'value2',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unsetProperties does not remove reserved properties', () => {
|
||||||
|
events.ownProperties.event_name = 'test'
|
||||||
|
events.unsetProperties('event_name')
|
||||||
|
|
||||||
|
expect(events.ownProperties.event_name).toBe('test')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('events include both shared and own properties', () => {
|
||||||
|
events.setProperty('customProp', 'custom value')
|
||||||
|
events.sendEvent('test_event')
|
||||||
|
|
||||||
|
expect(events.queue[0].properties).toEqual({
|
||||||
|
...mockSharedProperties.all,
|
||||||
|
customProp: 'custom value',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('event properties override own properties', () => {
|
||||||
|
events.setProperty('customProp', 'own value')
|
||||||
|
|
||||||
|
events.sendEvent('test_event', { customProp: 'event value' })
|
||||||
|
expect(events.queue[0].properties.customProp).toBe('event value')
|
||||||
|
})
|
||||||
|
})
|
||||||
258
tracker/tracker/src/main/modules/analytics/tests/people.test.ts
Normal file
258
tracker/tracker/src/main/modules/analytics/tests/people.test.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'
|
||||||
|
import People from '../people.js'
|
||||||
|
import * as utils from '../utils.js'
|
||||||
|
|
||||||
|
jest.spyOn(utils, 'isObject')
|
||||||
|
|
||||||
|
describe('People', () => {
|
||||||
|
let mockSharedProperties
|
||||||
|
let mockGetToken
|
||||||
|
let mockGetTimestamp
|
||||||
|
let people
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock shared properties
|
||||||
|
mockSharedProperties = {
|
||||||
|
all: {
|
||||||
|
$__os: 'Windows 10',
|
||||||
|
$__browser: 'Chrome 91.0.4472.124 (91)',
|
||||||
|
$__deviceId: 'test-device-id',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock token and timestamp functions
|
||||||
|
mockGetToken = jest.fn(() => 'test-token-123')
|
||||||
|
mockGetTimestamp = jest.fn(() => 1635186000000)
|
||||||
|
|
||||||
|
// Create People instance
|
||||||
|
people = new People(mockSharedProperties, mockGetToken, mockGetTimestamp)
|
||||||
|
|
||||||
|
// Mock fetch globally if needed for future implementations
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ success: true }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('constructor initializes with empty properties and null user_id', () => {
|
||||||
|
expect(people.ownProperties).toEqual({})
|
||||||
|
expect(people.user_id).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('identify sets user_id correctly', () => {
|
||||||
|
people.identify('test-user-123')
|
||||||
|
expect(people.user_id).toBe('test-user-123')
|
||||||
|
// Note: We're not testing the fetch endpoint as it's marked as TODO in the code
|
||||||
|
})
|
||||||
|
|
||||||
|
test('deleteUser resets user_id and properties', () => {
|
||||||
|
// Set up initial state
|
||||||
|
people.user_id = 'test-user-123'
|
||||||
|
people.ownProperties = {
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call deleteUser
|
||||||
|
people.deleteUser()
|
||||||
|
|
||||||
|
// Check results
|
||||||
|
expect(people.user_id).toBeNull()
|
||||||
|
expect(people.ownProperties).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setProperties adds properties correctly', () => {
|
||||||
|
people.setProperties({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
age: 30,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(people.ownProperties).toEqual({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
age: 30,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setProperties validates properties are objects', () => {
|
||||||
|
expect(() => {
|
||||||
|
people.setProperties('not an object')
|
||||||
|
}).toThrow('Properties must be an object')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setProperties ignores reserved properties', () => {
|
||||||
|
people.setProperties({
|
||||||
|
name: 'Test User',
|
||||||
|
distinct_id: 'should-be-ignored',
|
||||||
|
event_name: 'also-ignored',
|
||||||
|
properties: 'ignored-too',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(people.ownProperties).toEqual({
|
||||||
|
name: 'Test User',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reserved properties should not be present
|
||||||
|
expect(people.ownProperties.distinct_id).toBeUndefined()
|
||||||
|
expect(people.ownProperties.event_name).toBeUndefined()
|
||||||
|
expect(people.ownProperties.properties).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setPropertiesOnce only sets properties that do not exist', () => {
|
||||||
|
// Set initial property
|
||||||
|
people.ownProperties = {
|
||||||
|
name: 'Initial Name',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to set name again and add new properties
|
||||||
|
people.setPropertiesOnce({
|
||||||
|
name: 'New Name',
|
||||||
|
email: 'test@example.com',
|
||||||
|
age: 30,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(people.ownProperties).toEqual({
|
||||||
|
name: 'Initial Name', // Should not change
|
||||||
|
email: 'test@example.com', // Should be added
|
||||||
|
age: 30, // Should be added
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setPropertiesOnce validates properties are objects', () => {
|
||||||
|
expect(() => {
|
||||||
|
people.setPropertiesOnce('not an object')
|
||||||
|
}).toThrow('Properties must be an object')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('appendValues adds value to existing property turning it into an array', () => {
|
||||||
|
// Set initial string property
|
||||||
|
people.ownProperties = {
|
||||||
|
tags: 'tag1',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a value
|
||||||
|
people.appendValues('tags', 'tag2')
|
||||||
|
|
||||||
|
expect(people.ownProperties.tags).toEqual(['tag1', 'tag2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('appendValues adds value to existing array property', () => {
|
||||||
|
// Set initial array property
|
||||||
|
people.ownProperties = {
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a value
|
||||||
|
people.appendValues('tags', 'tag3')
|
||||||
|
|
||||||
|
expect(people.ownProperties.tags).toEqual(['tag1', 'tag2', 'tag3'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('appendValues does nothing if property does not exist', () => {
|
||||||
|
people.ownProperties = {}
|
||||||
|
|
||||||
|
people.appendValues('nonexistent', 'value')
|
||||||
|
|
||||||
|
expect(people.ownProperties.nonexistent).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('appendValues ignores reserved properties', () => {
|
||||||
|
people.ownProperties = {
|
||||||
|
distinct_id: 'reserved',
|
||||||
|
}
|
||||||
|
|
||||||
|
people.appendValues('distinct_id', 'new-value')
|
||||||
|
|
||||||
|
expect(people.ownProperties.distinct_id).toBe('reserved')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('appendUniqueValues adds unique value to array property', () => {
|
||||||
|
// Set initial array property
|
||||||
|
people.ownProperties = {
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a unique value
|
||||||
|
people.appendUniqueValues('tags', 'tag3')
|
||||||
|
|
||||||
|
expect(people.ownProperties.tags).toEqual(['tag1', 'tag2', 'tag3'])
|
||||||
|
|
||||||
|
// Try to append a duplicate value
|
||||||
|
people.appendUniqueValues('tags', 'tag2')
|
||||||
|
|
||||||
|
// Array should remain unchanged
|
||||||
|
expect(people.ownProperties.tags).toEqual(['tag1', 'tag2', 'tag3'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('appendUniqueValues adds unique value to string property', () => {
|
||||||
|
// Set initial string property
|
||||||
|
people.ownProperties = {
|
||||||
|
tag: 'tag1',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a different value
|
||||||
|
people.appendUniqueValues('tag', 'tag2')
|
||||||
|
|
||||||
|
expect(people.ownProperties.tag).toEqual(['tag1', 'tag2'])
|
||||||
|
|
||||||
|
// Try to append the same value
|
||||||
|
people.appendUniqueValues('tag', 'tag1')
|
||||||
|
|
||||||
|
// Array should remain unchanged
|
||||||
|
expect(people.ownProperties.tag).toEqual(['tag1', 'tag2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('appendUniqueValues does nothing if property does not exist', () => {
|
||||||
|
people.ownProperties = {}
|
||||||
|
|
||||||
|
people.appendUniqueValues('nonexistent', 'value')
|
||||||
|
|
||||||
|
expect(people.ownProperties.nonexistent).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('increment adds value to existing numerical property', () => {
|
||||||
|
// Set initial numerical property
|
||||||
|
people.ownProperties = {
|
||||||
|
count: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment it
|
||||||
|
people.increment('count', 5)
|
||||||
|
|
||||||
|
expect(people.ownProperties.count).toBe(15)
|
||||||
|
|
||||||
|
// Decrement it
|
||||||
|
people.increment('count', -3)
|
||||||
|
|
||||||
|
expect(people.ownProperties.count).toBe(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('increment does nothing for non-numerical properties', () => {
|
||||||
|
people.ownProperties = {
|
||||||
|
name: 'Test',
|
||||||
|
arrayProp: [1, 2, 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
people.increment('name', 5)
|
||||||
|
people.increment('arrayProp', 5)
|
||||||
|
|
||||||
|
expect(people.ownProperties.name).toBe('Test')
|
||||||
|
expect(people.ownProperties.arrayProp).toEqual([1, 2, 3])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('increment ignores reserved properties', () => {
|
||||||
|
people.ownProperties = {
|
||||||
|
distinct_id: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
people.increment('distinct_id', 5)
|
||||||
|
|
||||||
|
expect(people.ownProperties.distinct_id).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'
|
||||||
|
import SharedProperties from '../sharedProperties.js'
|
||||||
|
import * as utils from '../utils.js'
|
||||||
|
|
||||||
|
// Mock the utils module
|
||||||
|
jest.mock('../utils.js', () => ({
|
||||||
|
uaParse: jest.fn(),
|
||||||
|
isObject: jest.requireActual('../utils.js').isObject,
|
||||||
|
getUTCOffsetString: () => 'UTC+01:00'
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('SharedProperties', () => {
|
||||||
|
let mockApp
|
||||||
|
let mockWindow
|
||||||
|
let sessionStorage
|
||||||
|
let localStorage
|
||||||
|
|
||||||
|
const refKey = '$__initial_ref__$'
|
||||||
|
const distinctIdKey = '$__distinct_device_id__$'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create mock storage implementations
|
||||||
|
sessionStorage = {
|
||||||
|
[refKey]: 'https://example.com',
|
||||||
|
}
|
||||||
|
localStorage = {}
|
||||||
|
|
||||||
|
// Mock app with storage methods
|
||||||
|
mockApp = {
|
||||||
|
sessionStorage: {
|
||||||
|
getItem: jest.fn((key) => sessionStorage[key] || null),
|
||||||
|
setItem: jest.fn((key, value) => {
|
||||||
|
sessionStorage[key] = value
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
localStorage: {
|
||||||
|
getItem: jest.fn((key) => localStorage[key] || null),
|
||||||
|
setItem: jest.fn((key, value) => {
|
||||||
|
localStorage[key] = value
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock window
|
||||||
|
mockWindow = {
|
||||||
|
location: {
|
||||||
|
search: '?utm_source=test_source&utm_medium=test_medium&utm_campaign=test_campaign',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock document
|
||||||
|
global.document = {
|
||||||
|
referrer: 'https://example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock window globally to make it available to the constructor
|
||||||
|
global.window = mockWindow
|
||||||
|
|
||||||
|
// Setup mock data for uaParse
|
||||||
|
utils.uaParse.mockReturnValue({
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
browser: 'Chrome',
|
||||||
|
browserVersion: '91.0.4472.124',
|
||||||
|
browserMajorVersion: 91,
|
||||||
|
os: 'Windows',
|
||||||
|
osVersion: '10',
|
||||||
|
mobile: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset Math.random to ensure predictable device IDs for testing
|
||||||
|
jest
|
||||||
|
.spyOn(Math, 'random')
|
||||||
|
.mockReturnValueOnce(0.1) // First call
|
||||||
|
.mockReturnValueOnce(0.2) // Second call
|
||||||
|
.mockReturnValueOnce(0.3) // Third call
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
delete global.document
|
||||||
|
delete global.window
|
||||||
|
})
|
||||||
|
|
||||||
|
test('constructs with correct properties', () => {
|
||||||
|
const properties = new SharedProperties(mockApp.localStorage, mockApp.sessionStorage)
|
||||||
|
|
||||||
|
expect(utils.uaParse).toHaveBeenCalledWith(window)
|
||||||
|
expect(properties.os).toBe('Windows')
|
||||||
|
expect(properties.osVersion).toBe('10')
|
||||||
|
expect(properties.browser).toBe('Chrome')
|
||||||
|
expect(properties.browserVersion).toBe('91.0.4472.124 (91)')
|
||||||
|
expect(properties.device).toBe('Desktop')
|
||||||
|
expect(properties.screenHeight).toBe(1080)
|
||||||
|
expect(properties.screenWidth).toBe(1920)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects UTM parameters correctly', () => {
|
||||||
|
const properties = new SharedProperties(mockApp.localStorage, mockApp.sessionStorage)
|
||||||
|
|
||||||
|
expect(properties.utmSource).toBe('test_source')
|
||||||
|
expect(properties.utmMedium).toBe('test_medium')
|
||||||
|
expect(properties.utmCampaign).toBe('test_campaign')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles missing UTM parameters', () => {
|
||||||
|
mockWindow.location.search = ''
|
||||||
|
const properties = new SharedProperties(mockApp.localStorage, mockApp.sessionStorage)
|
||||||
|
|
||||||
|
expect(properties.utmSource).toBeNull()
|
||||||
|
expect(properties.utmMedium).toBeNull()
|
||||||
|
expect(properties.utmCampaign).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generates new device ID if none exists', () => {
|
||||||
|
const properties = new SharedProperties(mockApp.localStorage, mockApp.sessionStorage)
|
||||||
|
|
||||||
|
expect(mockApp.localStorage.getItem).toHaveBeenCalledWith(distinctIdKey)
|
||||||
|
expect(mockApp.localStorage.setItem).toHaveBeenCalled()
|
||||||
|
// (a-z0-9)\-(a-z0-9)\-(a-z0-9)
|
||||||
|
expect(properties.deviceId).toMatch(/^[a-z0-9]{6,12}-[a-z0-9]{6,12}-[a-z0-9]{6,12}$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('uses existing device ID if available', () => {
|
||||||
|
localStorage[distinctIdKey] = 'existing-device-id'
|
||||||
|
const properties = new SharedProperties(mockApp.localStorage, mockApp.sessionStorage)
|
||||||
|
|
||||||
|
expect(mockApp.localStorage.getItem).toHaveBeenCalledWith(distinctIdKey)
|
||||||
|
expect(mockApp.localStorage.setItem).not.toHaveBeenCalled()
|
||||||
|
expect(properties.deviceId).toBe('existing-device-id')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('gets referrer from session storage if available', () => {
|
||||||
|
sessionStorage[refKey] = 'https://stored-referrer.com'
|
||||||
|
const properties = new SharedProperties(mockApp.localStorage, mockApp.sessionStorage)
|
||||||
|
|
||||||
|
expect(mockApp.sessionStorage.getItem).toHaveBeenCalledWith(refKey)
|
||||||
|
expect(mockApp.sessionStorage.setItem).not.toHaveBeenCalled()
|
||||||
|
expect(properties.initialReferrer).toBe('https://stored-referrer.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns all properties with correct prefixes', () => {
|
||||||
|
const properties = new SharedProperties(mockApp.localStorage, mockApp.sessionStorage)
|
||||||
|
const allProps = properties.all
|
||||||
|
|
||||||
|
expect(allProps).toMatchObject({
|
||||||
|
$os: 'Windows',
|
||||||
|
$os_version: '10',
|
||||||
|
$browser: 'Chrome',
|
||||||
|
$browser_version: '91.0.4472.124 (91)',
|
||||||
|
$device: 'Desktop',
|
||||||
|
$screen_height: 1080,
|
||||||
|
$screen_width: 1920,
|
||||||
|
$initial_referrer: expect.stringMatching(/^https:\/\/example\.com$/),
|
||||||
|
$utm_source: 'test_source',
|
||||||
|
$utm_medium: 'test_medium',
|
||||||
|
$utm_campaign: 'test_campaign',
|
||||||
|
$distinct_id: expect.stringMatching(/^[a-z0-9]{6,12}-[a-z0-9]{6,12}-[a-z0-9]{6,12}$/),
|
||||||
|
$search_engine: null,
|
||||||
|
$user_id: null,
|
||||||
|
$device_id: expect.stringMatching(/^[a-z0-9]{6,12}-[a-z0-9]{6,12}-[a-z0-9]{6,12}$/),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles mobile device detection', () => {
|
||||||
|
utils.uaParse.mockReturnValue({
|
||||||
|
width: 375,
|
||||||
|
height: 812,
|
||||||
|
browser: 'Safari',
|
||||||
|
browserVersion: '14.1.1',
|
||||||
|
browserMajorVersion: 14,
|
||||||
|
os: 'iOS',
|
||||||
|
osVersion: '14.6',
|
||||||
|
mobile: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const properties = new SharedProperties(mockApp.localStorage, mockApp.sessionStorage)
|
||||||
|
|
||||||
|
expect(properties.device).toBe('Mobile')
|
||||||
|
expect(properties.os).toBe('iOS')
|
||||||
|
expect(properties.osVersion).toBe('14.6')
|
||||||
|
expect(properties.browser).toBe('Safari')
|
||||||
|
expect(properties.browserVersion).toBe('14.1.1 (14)')
|
||||||
|
})
|
||||||
|
})
|
||||||
131
tracker/tracker/src/main/modules/analytics/tests/utils.test.ts
Normal file
131
tracker/tracker/src/main/modules/analytics/tests/utils.test.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'
|
||||||
|
import { uaParse, isObject } from '../utils.js'
|
||||||
|
|
||||||
|
describe('isObject', () => {
|
||||||
|
test('returns true for objects', () => {
|
||||||
|
expect(isObject({})).toBe(true)
|
||||||
|
expect(isObject({ a: 1 })).toBe(true)
|
||||||
|
expect(isObject(new Object())).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false for non-objects', () => {
|
||||||
|
expect(isObject(null)).toBe(false)
|
||||||
|
expect(isObject(undefined)).toBe(false)
|
||||||
|
expect(isObject([])).toBe(false)
|
||||||
|
expect(isObject('string')).toBe(false)
|
||||||
|
expect(isObject(123)).toBe(false)
|
||||||
|
expect(isObject(true)).toBe(false)
|
||||||
|
expect(isObject(function () {})).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('uaParse', () => {
|
||||||
|
let originalNavigator
|
||||||
|
let originalScreen
|
||||||
|
let originalDocument
|
||||||
|
let mockWindow
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Save original objects
|
||||||
|
originalNavigator = global.navigator
|
||||||
|
originalScreen = global.screen
|
||||||
|
originalDocument = global.document
|
||||||
|
|
||||||
|
// Create mock screen
|
||||||
|
global.screen = {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup mock document
|
||||||
|
global.document = {
|
||||||
|
cookie: 'testcookie=1',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup mock window with basic navigator
|
||||||
|
mockWindow = {
|
||||||
|
navigator: {
|
||||||
|
appVersion: 'test version',
|
||||||
|
userAgent:
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
|
appName: 'Netscape',
|
||||||
|
cookieEnabled: true,
|
||||||
|
},
|
||||||
|
screen: {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
},
|
||||||
|
document: global.document,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global navigator
|
||||||
|
global.navigator = mockWindow.navigator
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original objects
|
||||||
|
global.navigator = originalNavigator
|
||||||
|
global.screen = originalScreen
|
||||||
|
global.document = originalDocument
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects Chrome browser correctly', () => {
|
||||||
|
const result = uaParse(mockWindow)
|
||||||
|
expect(result.browser).toBe('Chrome')
|
||||||
|
expect(result.browserMajorVersion).toBe(91)
|
||||||
|
expect(result.os).toBe('Windows')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects screen dimensions correctly', () => {
|
||||||
|
const result = uaParse(mockWindow)
|
||||||
|
expect(result.width).toBe(1920)
|
||||||
|
expect(result.height).toBe(1080)
|
||||||
|
expect(result.screen).toBe('1920 x 1080')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects mobile devices correctly', () => {
|
||||||
|
mockWindow.navigator.userAgent =
|
||||||
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1'
|
||||||
|
mockWindow.navigator.appVersion = 'iPhone'
|
||||||
|
const result = uaParse(mockWindow)
|
||||||
|
expect(result.mobile).toBe(true)
|
||||||
|
expect(result.os).toBe('iOS')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects Firefox browser correctly', () => {
|
||||||
|
mockWindow.navigator.userAgent =
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0'
|
||||||
|
const result = uaParse(mockWindow)
|
||||||
|
expect(result.browser).toBe('Firefox')
|
||||||
|
expect(result.browserMajorVersion).toBe(89)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects Edge browser correctly', () => {
|
||||||
|
mockWindow.navigator.userAgent =
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59'
|
||||||
|
const result = uaParse(mockWindow)
|
||||||
|
expect(result.browser).toBe('Microsoft Edge')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects cookies correctly', () => {
|
||||||
|
const result = uaParse(mockWindow)
|
||||||
|
expect(result.cookies).toBe(true)
|
||||||
|
|
||||||
|
mockWindow.navigator.cookieEnabled = false
|
||||||
|
const result2 = uaParse(mockWindow)
|
||||||
|
expect(result2.cookies).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles undefined screen dimensions', () => {
|
||||||
|
delete global.screen.width
|
||||||
|
delete global.screen.height
|
||||||
|
delete mockWindow.screen.width
|
||||||
|
delete mockWindow.screen.height
|
||||||
|
|
||||||
|
const result = uaParse(mockWindow)
|
||||||
|
expect(result.width).toBe(0)
|
||||||
|
expect(result.height).toBe(0)
|
||||||
|
expect(result.screen).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
250
tracker/tracker/src/main/modules/analytics/utils.ts
Normal file
250
tracker/tracker/src/main/modules/analytics/utils.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
interface ClientData {
|
||||||
|
screen: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
browser: string
|
||||||
|
browserVersion: string
|
||||||
|
browserMajorVersion: number
|
||||||
|
mobile: boolean
|
||||||
|
os: string
|
||||||
|
osVersion: string
|
||||||
|
cookies: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientOS {
|
||||||
|
s: string
|
||||||
|
r: RegExp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects client browser, OS, and device information
|
||||||
|
*/
|
||||||
|
export function uaParse(sWindow: Window & typeof globalThis): ClientData {
|
||||||
|
const unknown = '-'
|
||||||
|
|
||||||
|
// Screen detection
|
||||||
|
let width: number = 0
|
||||||
|
let height: number = 0
|
||||||
|
let screenSize = ''
|
||||||
|
|
||||||
|
if (sWindow.screen.width) {
|
||||||
|
width = sWindow.screen.width
|
||||||
|
height = sWindow.screen.height
|
||||||
|
screenSize = `${width} x ${height}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser detection
|
||||||
|
const nVer: string = sWindow.navigator.appVersion
|
||||||
|
const nAgt: string = sWindow.navigator.userAgent
|
||||||
|
let browser: string = sWindow.navigator.appName
|
||||||
|
let version: string = String(parseFloat(nVer))
|
||||||
|
let nameOffset: number
|
||||||
|
let verOffset: number
|
||||||
|
let ix: number
|
||||||
|
|
||||||
|
// Browser detection logic
|
||||||
|
if ((verOffset = nAgt.indexOf('YaBrowser')) !== -1) {
|
||||||
|
browser = 'Yandex'
|
||||||
|
version = nAgt.substring(verOffset + 10)
|
||||||
|
} else if ((verOffset = nAgt.indexOf('SamsungBrowser')) !== -1) {
|
||||||
|
browser = 'Samsung'
|
||||||
|
version = nAgt.substring(verOffset + 15)
|
||||||
|
} else if ((verOffset = nAgt.indexOf('UCBrowser')) !== -1) {
|
||||||
|
browser = 'UC Browser'
|
||||||
|
version = nAgt.substring(verOffset + 10)
|
||||||
|
} else if ((verOffset = nAgt.indexOf('OPR')) !== -1) {
|
||||||
|
browser = 'Opera'
|
||||||
|
version = nAgt.substring(verOffset + 4)
|
||||||
|
} else if ((verOffset = nAgt.indexOf('Opera')) !== -1) {
|
||||||
|
browser = 'Opera'
|
||||||
|
version = nAgt.substring(verOffset + 6)
|
||||||
|
if ((verOffset = nAgt.indexOf('Version')) !== -1) {
|
||||||
|
version = nAgt.substring(verOffset + 8)
|
||||||
|
}
|
||||||
|
} else if ((verOffset = nAgt.indexOf('Edge')) !== -1) {
|
||||||
|
browser = 'Microsoft Legacy Edge'
|
||||||
|
version = nAgt.substring(verOffset + 5)
|
||||||
|
} else if ((verOffset = nAgt.indexOf('Edg')) !== -1) {
|
||||||
|
browser = 'Microsoft Edge'
|
||||||
|
version = nAgt.substring(verOffset + 4)
|
||||||
|
} else if ((verOffset = nAgt.indexOf('MSIE')) !== -1) {
|
||||||
|
browser = 'Microsoft Internet Explorer'
|
||||||
|
version = nAgt.substring(verOffset + 5)
|
||||||
|
} else if ((verOffset = nAgt.indexOf('Chrome')) !== -1) {
|
||||||
|
browser = 'Chrome'
|
||||||
|
version = nAgt.substring(verOffset + 7)
|
||||||
|
} else if ((verOffset = nAgt.indexOf('Safari')) !== -1) {
|
||||||
|
browser = 'Safari'
|
||||||
|
version = nAgt.substring(verOffset + 7)
|
||||||
|
if ((verOffset = nAgt.indexOf('Version')) !== -1) {
|
||||||
|
version = nAgt.substring(verOffset + 8)
|
||||||
|
}
|
||||||
|
} else if ((verOffset = nAgt.indexOf('Firefox')) !== -1) {
|
||||||
|
browser = 'Firefox'
|
||||||
|
version = nAgt.substring(verOffset + 8)
|
||||||
|
} else if (nAgt.indexOf('Trident/') !== -1) {
|
||||||
|
browser = 'Microsoft Internet Explorer'
|
||||||
|
version = nAgt.substring(nAgt.indexOf('rv:') + 3)
|
||||||
|
} else if ((nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/'))) {
|
||||||
|
browser = nAgt.substring(nameOffset, verOffset)
|
||||||
|
version = nAgt.substring(verOffset + 1)
|
||||||
|
if (browser.toLowerCase() === browser.toUpperCase()) {
|
||||||
|
browser = sWindow.navigator.appName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim the version string
|
||||||
|
if ((ix = version.indexOf(';')) !== -1) {
|
||||||
|
version = version.substring(0, ix)
|
||||||
|
}
|
||||||
|
if ((ix = version.indexOf(' ')) !== -1) {
|
||||||
|
version = version.substring(0, ix)
|
||||||
|
}
|
||||||
|
if ((ix = version.indexOf(')')) !== -1) {
|
||||||
|
version = version.substring(0, ix)
|
||||||
|
}
|
||||||
|
|
||||||
|
let majorVersion: number = parseInt(version, 10)
|
||||||
|
if (isNaN(majorVersion)) {
|
||||||
|
version = String(parseFloat(nVer))
|
||||||
|
majorVersion = parseInt(nVer, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile detection
|
||||||
|
const mobile: boolean = /Mobile|mini|Fennec|Android|iP(ad|od|hone)/.test(nVer)
|
||||||
|
|
||||||
|
// Cookie detection
|
||||||
|
let cookieEnabled: boolean = sWindow.navigator.cookieEnabled || false
|
||||||
|
|
||||||
|
if (typeof navigator.cookieEnabled === 'undefined' && !cookieEnabled) {
|
||||||
|
sWindow.document.cookie = 'testcookie'
|
||||||
|
cookieEnabled = sWindow.document.cookie.indexOf('testcookie') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// OS detection
|
||||||
|
let os: string = unknown
|
||||||
|
const clientStrings: ClientOS[] = [
|
||||||
|
{ s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ },
|
||||||
|
{ s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ },
|
||||||
|
{ s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ },
|
||||||
|
{ s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ },
|
||||||
|
{ s: 'Windows Vista', r: /Windows NT 6.0/ },
|
||||||
|
{ s: 'Windows Server 2003', r: /Windows NT 5.2/ },
|
||||||
|
{ s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ },
|
||||||
|
{ s: 'Windows 2000', r: /(Windows NT 5.0|Windows 2000)/ },
|
||||||
|
{ s: 'Windows ME', r: /(Win 9x 4.90|Windows ME)/ },
|
||||||
|
{ s: 'Windows 98', r: /(Windows 98|Win98)/ },
|
||||||
|
{ s: 'Windows 95', r: /(Windows 95|Win95|Windows_95)/ },
|
||||||
|
{ s: 'Windows NT 4.0', r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/ },
|
||||||
|
{ s: 'Windows CE', r: /Windows CE/ },
|
||||||
|
{ s: 'Windows 3.11', r: /Win16/ },
|
||||||
|
{ s: 'Android', r: /Android/ },
|
||||||
|
{ s: 'Open BSD', r: /OpenBSD/ },
|
||||||
|
{ s: 'Sun OS', r: /SunOS/ },
|
||||||
|
{ s: 'Chrome OS', r: /CrOS/ },
|
||||||
|
{ s: 'Linux', r: /(Linux|X11(?!.*CrOS))/ },
|
||||||
|
{ s: 'iOS', r: /(iPhone|iPad|iPod)/ },
|
||||||
|
{ s: 'Mac OS X', r: /Mac OS X/ },
|
||||||
|
{ s: 'Mac OS', r: /(Mac OS|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ },
|
||||||
|
{ s: 'QNX', r: /QNX/ },
|
||||||
|
{ s: 'UNIX', r: /UNIX/ },
|
||||||
|
{ s: 'BeOS', r: /BeOS/ },
|
||||||
|
{ s: 'OS/2', r: /OS\/2/ },
|
||||||
|
{
|
||||||
|
s: 'Search Bot',
|
||||||
|
r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Find matching OS
|
||||||
|
for (const client of clientStrings) {
|
||||||
|
if (client.r.test(nAgt)) {
|
||||||
|
os = client.s
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OS Version detection
|
||||||
|
let osVersion: string = unknown
|
||||||
|
|
||||||
|
if (/Windows/.test(os)) {
|
||||||
|
const matches = /Windows (.*)/.exec(os)
|
||||||
|
if (matches && matches[1]) {
|
||||||
|
osVersion = matches[1]
|
||||||
|
// Handle Windows 10/11 detection with newer API if available
|
||||||
|
if (osVersion === '10' && 'userAgentData' in sWindow.navigator) {
|
||||||
|
const nav = navigator as Navigator & {
|
||||||
|
userAgentData?: {
|
||||||
|
getHighEntropyValues(values: string[]): Promise<{ platformVersion: string }>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nav.userAgentData) {
|
||||||
|
nav.userAgentData
|
||||||
|
.getHighEntropyValues(['platformVersion'])
|
||||||
|
.then((ua) => {
|
||||||
|
const version = parseInt(ua.platformVersion.split('.')[0], 10)
|
||||||
|
osVersion = version < 13 ? '10' : '11'
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Fallback if high entropy values not available
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os = 'Windows'
|
||||||
|
}
|
||||||
|
|
||||||
|
// OS version detection for Mac/Android/iOS
|
||||||
|
switch (os) {
|
||||||
|
case 'Mac OS':
|
||||||
|
case 'Mac OS X':
|
||||||
|
case 'Android': {
|
||||||
|
const matches =
|
||||||
|
/(?:Android|Mac OS|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh) ([\.\_\d]+)/.exec(nAgt)
|
||||||
|
osVersion = matches && matches[1] ? matches[1] : unknown
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'iOS': {
|
||||||
|
const matches = /OS (\d+)_(\d+)_?(\d+)?/.exec(nVer)
|
||||||
|
if (matches && matches[1]) {
|
||||||
|
osVersion = `${matches[1]}.${matches[2]}.${parseInt(matches[3] || '0', 10)}`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return client data
|
||||||
|
return {
|
||||||
|
screen: screenSize,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
browser,
|
||||||
|
browserVersion: version,
|
||||||
|
browserMajorVersion: majorVersion,
|
||||||
|
mobile,
|
||||||
|
os,
|
||||||
|
osVersion,
|
||||||
|
cookies: cookieEnabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObject(item: any): boolean {
|
||||||
|
const isNull = item === null
|
||||||
|
return Boolean(item && typeof item === 'object' && !Array.isArray(item) && !isNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUTCOffsetString() {
|
||||||
|
const date = new Date()
|
||||||
|
const offsetMinutes = date.getTimezoneOffset()
|
||||||
|
|
||||||
|
const hours = Math.abs(Math.floor(offsetMinutes / 60))
|
||||||
|
const minutes = Math.abs(offsetMinutes % 60)
|
||||||
|
|
||||||
|
const sign = offsetMinutes <= 0 ? '+' : '-'
|
||||||
|
|
||||||
|
const hoursStr = hours.toString().padStart(2, '0')
|
||||||
|
const minutesStr = minutes.toString().padStart(2, '0')
|
||||||
|
|
||||||
|
return `UTC${sign}${hoursStr}:${minutesStr}`
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,9 @@ export class StringDictionary {
|
||||||
|
|
||||||
getKey = (str: string): [number, boolean] => {
|
getKey = (str: string): [number, boolean] => {
|
||||||
let isNew = false
|
let isNew = false
|
||||||
if (!this.backDict[str]) {
|
// avoiding potential native object properties
|
||||||
|
const safeKey = `__${str}`
|
||||||
|
if (!this.backDict[safeKey]) {
|
||||||
isNew = true
|
isNew = true
|
||||||
// shaving the first 2 digits of the timestamp (since they are irrelevant for next millennia)
|
// shaving the first 2 digits of the timestamp (since they are irrelevant for next millennia)
|
||||||
const shavedTs = Date.now() % 10 ** (13 - 2)
|
const shavedTs = Date.now() % 10 ** (13 - 2)
|
||||||
|
|
@ -26,10 +28,10 @@ export class StringDictionary {
|
||||||
} else {
|
} else {
|
||||||
this.lastSuffix = 1
|
this.lastSuffix = 1
|
||||||
}
|
}
|
||||||
this.backDict[str] = id
|
this.backDict[safeKey] = id
|
||||||
this.lastTs = shavedTs
|
this.lastTs = shavedTs
|
||||||
}
|
}
|
||||||
return [this.backDict[str], isNew]
|
return [this.backDict[safeKey], isNew]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ describe('Singleton Testing', () => {
|
||||||
singleton.configure(options);
|
singleton.configure(options);
|
||||||
|
|
||||||
methods.forEach(method => {
|
methods.forEach(method => {
|
||||||
console.log(method);
|
|
||||||
expect(singleton[method]).toBeDefined();
|
expect(singleton[method]).toBeDefined();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue