First Commit

This commit is contained in:
2024-09-04 21:35:55 +05:30
commit 2072243561
19 changed files with 1429 additions and 0 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
NODE_ENV=production

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
dist
node_modules

6
.npmignore Normal file
View File

@@ -0,0 +1,6 @@
tsconfig*
package-lock.json
build.ts
build.js
node_modules
index.ts

618
analytics.ts Normal file
View File

@@ -0,0 +1,618 @@
import * as fs from 'fs';
import * as path from 'path';
import $ from "jquery";
import { v4 as uuidv4 } from "uuid";
import { ADD, ARMCO_SERVER } from "./constants";
import { startSession, getSessionId, terminateSession } from "./session";
import { ipLookup, success, error, localTimeRegion } from "./location";
import { isArClient } from './helper';
import { hookFlushHandlers, queueEvent } from './flush';
import { User, Event, ConfigType, objType } from "./index.interface";
const tsConfigPath = getEnvironmentType() === "node" ? path.resolve(process.cwd(), 'tsconfig.json') : "../../../../tsConfig.json";
const packageJsonPath = getEnvironmentType() === "node" ? path.resolve(process.cwd(), 'package.json') : "../../../../package.json";
let ar_anonymous_id: string | null;
let user: User | null = null;
let apiKey: string | null = null;
let analyticsLogEndpoint: string | null;
let analyticsTagEndpoint: string | null;
let hostProjectName: string | null = null; // Will be set during initialization
let enabled: boolean | null = null;
const CONFIG_FILE_NAME = "analyticsrc";
let region: string | null;
let address: string | null;
let coordinates: {
latitude: number
longitude: number
}
let runtime: string | null = null
let CONFIG: ConfigType | null | undefined;
let environment: "node" | "browser" | "unknown";
async function isTypeScriptProject(): Promise<boolean> {
// Check if tsconfig.json file exists
if (fs && "existsSync" in fs && !(fs.existsSync(tsConfigPath) && fs.existsSync(packageJsonPath))) {
return false;
}
let tsConfig, packageJson;
try {
tsConfig = await import(tsConfigPath);
// Check if "compilerOptions" property exists in tsconfig.json
if (tsConfig && tsConfig.compilerOptions) {
return true;
}
} catch (error) {
console.error('Error loading tsconfig.json:', error);
}
// Check if package.json file has TypeScript as a devDependency
try {
packageJson = await import(tsConfigPath);
if (
packageJson &&
packageJson.devDependencies &&
packageJson.devDependencies.typescript
) {
return true;
}
} catch (error) {
console.error('Error loading tsconfig.json:', error);
}
if (
packageJson &&
packageJson.devDependencies &&
packageJson.devDependencies.typescript
) {
return true;
}
return false;
}
async function loadConfiguration() {
// Path of this module when deployed as NPM lib in development will be <src_root>/node_modules/@armco/analytics/dist/analytics.js,
// hence need to move 3 steps up to reach project root, but lib name is @armco/analytics, this is considered as
// additional path, hence we move 4 steps up, for production we search two steps up, this function should only be called when
// the user fails to provide a configuration manually.
const ROOT = (runtime || getEnvironment()) === "production" ? "../../" : "../../../../"
let configFilePath = `${ROOT}${CONFIG_FILE_NAME}.json`;
try {
const config = await import(configFilePath);
return config; // Return the configuration if successful
} catch (error) {
console.error(`[ANALYTICS] Failed to load configuration from ${configFilePath}.`);
}
const isTS = await isTypeScriptProject();
configFilePath = `${ROOT}${CONFIG_FILE_NAME}.${isTS ? "ts" : "js"}`;
try {
const config = await import(configFilePath);
console.log(`[ANALYTICS] Configuration loaded from ${configFilePath} successfully.`);
return config.default; // Return the configuration if successful
} catch (error) {
console.error(`[ANALYTICS] Failed to load configuration from ${configFilePath}.`);
}
console.error(`[ANALYTICS] No valid configuration file found. Expected one of ${CONFIG_FILE_NAME}.js, ${CONFIG_FILE_NAME}.json or ${CONFIG_FILE_NAME}.ts`);
return null;
}
function getEnvironment(): string {
// Check if 'process' object is available (Webpack)
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV) {
return process.env.NODE_ENV;
}
// Check if 'import.meta' object is available (Vite)
if ((import.meta as any).env && (import.meta as any).env.MODE) {
return (import.meta as any).env.MODE;
}
// If no environment is found, return a default value
return 'development';
}
function getEnvironmentType(): 'browser' | 'node' | 'unknown' {
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
return 'browser';
} else if (
typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null
) {
return 'node';
} else {
return 'unknown';
}
}
// Function to get the name of the host project
async function getHostProjectName(): Promise<string | null> {
if (!hostProjectName) {
try {
const packageJson = await import(packageJsonPath); // Adjust the path if necessary
hostProjectName = packageJson.name || null;
console.log("[ANALYTICS] Fetched project name from package details: ", hostProjectName);
enabled = isEnabled();
} catch (e) {
console.warn("[ANALYTICS] Failed to fetch project name, continuing without")
}
}
return hostProjectName;
}
function enrichEventData(data: objType) {
if (data) {
data.eventId = uuidv4();
data.client = hostProjectName;
data.sessionId = getSessionId();
data.url = window.location.href;
region && (data.region = region);
address && !data.address && (data.address = address);
coordinates && !data.coordinates && (data.coordinates = coordinates);
!data.timestamp && (data.timestamp = new Date());
!data.userId && (data.userId = (user ? user.email : ar_anonymous_id));
!data.email && user && user.email && (data.email = user.email);
user && (data.user = user);
}
}
function isMalformedEvent(data: objType) {
return !data || !data.eventType;
}
// Function to send bulk events to the analaytics server
export async function sendBulkData(data: Event[], callback?: Function): Promise<void> {
const promises = data && data.map((event: Event) => sendAnalyticsData(event));
Promise.all(promises).then(() => callback && callback())
}
// Function to send data to the analytics server
async function sendAnalyticsData(data: objType): Promise<void> {
try {
if (!apiKey && !analyticsLogEndpoint) {
console.error('Neither of API key and Analytics server configured. Data not sent.');
return;
}
const options: {
method: string,
headers: {
"Content-Type": string
Authorization?: string
},
body: any
} = {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ event: data }),
};
if (apiKey) {
options.headers.Authorization = `Bearer ${apiKey}`;
}
const logEndpoint = apiKey ? ARMCO_SERVER + ADD : analyticsLogEndpoint as string;
try {
const response = await fetch(logEndpoint, options);
console.log('Analytics data sent to server:', logEndpoint, data, response.status, response.statusText);
} catch (e) {
console.warn("Failed attempt to log event");
}
} catch (error) {
console.error('Failed to send analytics data:', error);
}
}
async function tagEvents(email: string) {
try {
if (!apiKey && !analyticsTagEndpoint) {
console.error('Neither of API key and Analytics server configured. Tagging won\'t be attempted.');
return;
}
const options: {
method: string,
headers: {
"Content-Type": string
Authorization?: string
},
body: any
} = {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: { event: JSON.stringify({ email, anonymousId: ar_anonymous_id }) },
};
if (apiKey) {
options.headers.Authorization = `Bearer ${apiKey}`;
}
const tagEndpoint = apiKey ? ARMCO_SERVER + ADD : analyticsTagEndpoint as string;
const response = await fetch(tagEndpoint, options);
console.log('Analytics data sent to server:', tagEndpoint, response.status, response.statusText);
} catch (error) {
console.error('Failed to send analytics data:', error);
}
}
// Core function to track a generic event
function trackEvent(event: string | Event, data?: any): void {
const tracker = user && user.email ? user.email : ar_anonymous_id;
if (enabled && tracker) {
if (typeof event === "string") {
data = data || {}
if (data) {
data.eventType = event;
}
} else {
data = event;
}
if (isMalformedEvent(data)) {
console.error("Attempting to send empty event, or event missing eventType, failing send...");
return;
}
enrichEventData(data);
CONFIG?.submissionStrategy === "DEFER" ? queueEvent(data) : sendAnalyticsData(data);
} else {
console.log('Analytics disabled or user not identified. Event data not sent.');
}
}
// Wrapper function to track page views
function trackPageView(pageName: string, data?: { [key: string]: any }): void {
const pageViewEvent: Event = {
eventType: "Page View",
pageName,
...data
// Other page-specific properties...
};
trackEvent('Page View', pageViewEvent);
}
// Wrapper function to track errors
function trackError(errorMessage: string): void {
const errorEvent: Event = {
eventType: 'Error',
timestamp: new Date(),
errorMessage,
// Other error-specific properties...
};
trackEvent(errorEvent);
}
// Function to determine if tracking is enabled or disabled
function isEnabled(): boolean {
// Check if navigator and navigator.doNotTrack are available
if (typeof navigator !== 'undefined' && 'doNotTrack' in navigator) {
// Check if the "doNotTrack" property is "1" (meaning "yes, do not track")
// or "yes" (some older browsers use this instead of "1")
const doNotTrackValue = navigator.doNotTrack;
if (doNotTrackValue === '1' || doNotTrackValue === 'yes') {
console.warn("[ANALYTICS] Tracking disabled in client, events will not be logged!")
// Tracking is disabled, so return false
return isArClient(hostProjectName) || false;
}
}
// If the navigator.doNotTrack property is not available or its value is not "1" or "yes",
// then return true (i.e., tracking is enabled by default).
return true;
}
// Function to enable or disable tracking
function enableTracking(enable: boolean): void {
enabled = isEnabled() && enable;
}
// Function to generate an anonymous ID for users who haven't logged in
function generateAnonymousId(): string {
ar_anonymous_id = uuidv4();
return ar_anonymous_id;
}
function populateLocationDetails() {
if (!navigator.geolocation) {
console.warn("Geolocation is not supported by your browser");
ipLookup();
} else {
navigator.geolocation.getCurrentPosition((position: { coords: { latitude: number, longitude: number } }) => {
coordinates = { latitude: position.coords.latitude, longitude: position.coords.longitude }
success(position, (reverseGeocodingResponse: any) => {
address = reverseGeocodingResponse.results[0].formatted_address
});
}, error);
}
region = localTimeRegion;
}
const trackedItems = [
"a[href]",
"button",
"input[type='button']",
"input[type='submit']",
"input[type='reset']",
"input[type='checkbox']",
"input[type='radio']",
"select",
"textarea",
"area",
"details",
"summary",
"iframe",
"object",
"embed",
"label",
"img",
"[role='button']",
"[role='checkbox']",
"[role='link']",
"[role='menuitem']",
"[role='menuitemcheckbox']",
"[role='menuitemradio']",
"[data-track='true']"
];
function isClickable(element: HTMLElement) {
return element.matches(trackedItems.join(", ")) ||
(element as HTMLButtonElement).onclick != null ||
window.getComputedStyle(element).cursor == "pointer";
}
function handleTrackedItemClick(e: MouseEvent) {
const clickedElement = e.target as HTMLElement;
if (isClickable(clickedElement)) {
const dataAttributes = { ...clickedElement.dataset };
const id = clickedElement.id || null;
const name = clickedElement.getAttribute('name') || null;
const classes = Array.from(clickedElement.classList);
const elementType = clickedElement.tagName.toLowerCase();
const textContent = clickedElement.textContent;
const href = 'href' in clickedElement ? (clickedElement as HTMLAnchorElement).href : null;
const role = clickedElement.getAttribute('role') || null;
const parentElementId = clickedElement.parentElement?.id || null;
const value =
'value' in clickedElement && (clickedElement as HTMLInputElement).value
? (clickedElement as HTMLInputElement).value
: null;
// Merge dataAttributes (if present) with identified properties
const mergedData = {
...dataAttributes,
id,
name,
classes,
textContent,
href,
tagName: elementType,
role,
parentElementId,
value,
}
trackEvent("CLICK", {element: mergedData});
}
}
function hookTrackers() {
if (environment === "browser") {
const TRACK_EVENTS = CONFIG?.trackEvents || ["click", "submit", "select-change"];
if (TRACK_EVENTS.indexOf("click") > -1) {
console.log("[ANALYTICS] Attaching Click handlers")
const clickables = Array.from(document.querySelectorAll('*'))
console.log("[ANALYTICS] Found " + clickables.length + " items that can be clicked!")
console.log("[ANALYTICS] Dynamically added elements will be added to this list.")
document.addEventListener("click", handleTrackedItemClick)
console.log("[ANALYTICS] Click handlers Attached")
}
if (TRACK_EVENTS.indexOf("submit") > -1) {
console.log("[ANALYTICS] Attaching Submit handlers")
// Add a global event listener for the "submit" event on the document
document.addEventListener('submit', event => {
// Get the form element that triggered the submit event
const formElement = (event.target as HTMLFormElement);
// Get additional information from the form, if needed
// For example, you can get form data, form ID, or any other relevant information
// Track the submit event
trackEvent('SUBMIT', {
submit: {
formId: formElement.id,
formData: new FormData(formElement)
}
});
});
console.log("[ANALYTICS] Submit handlers attached")
}
if (TRACK_EVENTS.indexOf("select-change") > -1) {
// Add a global event listener for the "change" event on the document
console.log("[ANALYTICS] Attaching Select OnChange handlers")
document.addEventListener('change', (event) => {
const target = event.target as HTMLElement;
// Check if the target element is a select element
if (target.tagName === 'SELECT') {
// Get the selected option's value and label
const selectedOptionValue = (target as HTMLSelectElement).value;
const selectedOptionLabel = (target as HTMLSelectElement).selectedOptions[0].label;
// Track the select change event
trackEvent('SELECT_CHANGE', { value: selectedOptionValue, label: selectedOptionLabel });
}
});
console.log("[ANALYTICS] Select OnChange handlers attached")
}
}
}
// Function to identify the user with an email before login
function identify(appUser: User): void {
if (user === null) {
// If the user is not already identified, create a new user object
user = appUser;
} else {
// If the user is already identified, update the email
user.email = appUser.email;
}
tagEvents(user.email);
ar_anonymous_id = null;
}
// Function to send the host project name to the analytics server
function sendHostProjectName(): void {
if (hostProjectName) {
const data = {
hostProjectName,
};
sendAnalyticsData(data);
}
}
function showTrackingPopup() {
const popupContent = `
<div class="tracking-popup" style="position: fixed;
width: 50%;
left: 25%;
bottom: 2rem;
padding: 1rem;
background: aliceblue;
border-radius: 5px;
display: flex;
border: 1px solid #ccc;
justify-content: space-between;">
<p style="width: 80%;">We use cookies to collect and analyze data to improve our website. By clicking "Accept," you consent to the use of cookies.</p>
<a class="btn-accept" style="display: flex;
align-items: center;
font-weight: bold;
cursor: pointer;"
onClick="${(e: any) => {
e.preventDefault();
enableTracking(true);
}};"
>Got it!</a>
</div>
`;
$('body').append(popupContent);
// Add event listeners to the buttons
$('.btn-accept').on('click', function () {
// Handle user consent (e.g., start tracking events)
$('.tracking-popup').remove(); // Remove the popup after user consent
});
$('.btn-decline').on('click', function () {
// Handle user decline (e.g., stop tracking events)
enableTracking(false);
$('.tracking-popup').remove(); // Remove the popup after user decline
});
}
function loadAnalytics(config?: ConfigType) {
console.log("[ANALYTICS] Loading Configuration");
CONFIG = config;
if (CONFIG) {
// update foundational config
console.log("[ANALYTICS] Configuration loaded")
apiKey = CONFIG.apiKey || null;
analyticsLogEndpoint = CONFIG.analyticsLogEndpoint || null;
analyticsTagEndpoint = CONFIG.analyticsTagEndpoint || null;
hostProjectName = CONFIG.hostProjectName || hostProjectName || null; // Automatically get the host project name
console.log("[ANALYTICS] Check if Analytics enabled");
enabled = isEnabled()
if (enabled) {
console.log("[ANALYTICS] Display Tracker Popup")
// Show the tracking popup
CONFIG.showPopUp && showTrackingPopup();
console.log("[ANALYTICS] Hook Event Trackers")
// Attach tracking to specified events
hookTrackers();
console.log("[ANALYTICS] Hook Handlers to flush events (use when submissionStrategy is configured as \"DEFER\"")
// Establish event submission strategy
hookFlushHandlers(CONFIG.submissionStrategy);
console.log("[ANALYTICS] Find User Location Details")
// save location details
populateLocationDetails();
console.log("[ANALYTICS] Initiate Session and Anonymous User ID")
// Start Session
const anonId = generateAnonymousId()
console.log("Tracking User as", anonId);
startSession();
window.addEventListener('load', function() {
console.log("[ANALYTICS] Logging page load");
trackEvent("PAGE");
});
window.addEventListener("popstate", (event) => {
const url = window.location.href;
trackEvent("NAV", { url });
});
} else {
console.warn("[ANALYTICS] Analytics blocked by client, or disabled by configuration!")
}
}
}
// Initialize the library with the configuration
function init(config?: ConfigType): void {
try {
environment = getEnvironmentType();
runtime = getEnvironment();
const projectName = config && config.hostProjectName;
if (projectName) {
hostProjectName = projectName;
loadAnalytics(config);
} else {
getHostProjectName()
.then((projectName) => {
hostProjectName = projectName
if (config) {
loadAnalytics(config);
} else {
loadConfiguration()
.then(config => {
loadAnalytics(config);
});
}
})
.catch((err) => {
console.error("[ANALYTICS] Couldn't determine host project name, no events will be logged!")
});
}
} catch (e) {
console.log("[ANALYTICS]", e);
}
}
// Function to logout the current session
function logout(): void {
if (user) {
user = null;
generateAnonymousId();
}
terminateSession();
}
// Export the utility functions for use in your analytics library
export {
enabled as analyticsEnabled,
init,
identify,
getEnvironmentType,
getEnvironment,
loadConfiguration,
logout,
trackEvent,
trackPageView,
trackError,
enableTracking,
generateAnonymousId,
sendHostProjectName,
getSessionId,
startSession,
};

57
build.js Normal file
View File

@@ -0,0 +1,57 @@
/**
* Remove old files, copy front-end ones.
*/
import fs from "fs-extra";
import childProcess from "child_process";
import pkg from "./package.json" assert {type: "json"};
/**
* Start
*/
(async () => {
try {
// Remove current
console.log("removing dist");
await remove("./dist/");
await exec("tsc --build tsconfig.prod.json", "./");
pkg.scripts = {};
pkg.devDependencies = {};
if (pkg.main.startsWith("dist/")) {
pkg.main = pkg.main.slice(5);
}
fs.outputFileSync(
"./dist/package.json",
Buffer.from(JSON.stringify(pkg, null, 2), "utf-8")
);
fs.copyFileSync(".npmignore", "./dist/.npmignore");
fs.copyFileSync("global-modules.d.ts", "./dist/global-modules.d.ts");
console.log("Trigger build");
} catch (err) {
console.log(err);
process.exit(1);
}
})();
/**
* Remove file
*/
function remove(loc) {
return new Promise((res, rej) => {
return fs.remove(loc, (err) => {
return !!err ? rej(err) : res();
});
});
}
/**
* Do command line command.
*/
function exec(cmd, loc) {
return new Promise((res, rej) => {
return childProcess.exec(cmd, {cwd: loc}, (err, stdout, stderr) => {
return !!err ? rej(err) : res();
});
});
}

3
constants.ts Normal file
View File

@@ -0,0 +1,3 @@
export const ARMCO_SERVER = "https://telemetry.armco.tech/events/";
export const ADD = "add";
export const TAG = "tag"

55
flush.ts Normal file
View File

@@ -0,0 +1,55 @@
import { sendBulkData } from "./analytics";
import { Event, SubmissionStrategy } from "./index.interface";
const MAX_EVENTS = 100; // Maximum number of events to collect before flushing
const FLUSH_INTERVAL = 15000; // 60 seconds (in milliseconds)
let events: Event[] = [];
// Function to add an event to the collection
export function queueEvent(event: Event) {
events.push(event);
if (events.length >= MAX_EVENTS) {
flushEvents();
}
}
// Function to flush the collected events
function flushEvents() {
// Implement the logic to send the events to your analytics server here
sendBulkData(events, () => {
// Clear the events array after flushing
events = [];
})
}
// Attach event listeners (if running in a browser environment)
export function hookFlushHandlers(submissionStrategy: SubmissionStrategy = "ONEVENT") {
if (typeof window !== 'undefined' && submissionStrategy === "DEFER") {
// Function to flush events before navigating away
function handleBeforeUnload() {
if (events.length > 0) {
flushEvents();
}
}
// Function to flush events when changing tabs
function handleVisibilityChange() {
if (document.visibilityState === 'hidden' && events.length > 0) {
flushEvents();
}
}
// Attach event listeners
window.addEventListener('beforeunload', handleBeforeUnload);
document.addEventListener('visibilitychange', handleVisibilityChange);
// Flush events on a regular interval
setInterval(() => {
if (events.length > 0) {
flushEvents();
}
}, FLUSH_INTERVAL);
}
}

14
global-modules.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import analytics from './index';
declare global {
namespace NodeJS {
interface Global {
analytics: analytics;
}
}
interface Window {
analytics: analytics;
}
}
export {};

3
helper.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isArClient(name: string | null): boolean {
return !!(name && name.startsWith("@armco"));
}

32
index.interface.ts Normal file
View File

@@ -0,0 +1,32 @@
export interface User {
email: string;
// Other user properties...
}
export interface Event {
eventType: string;
timestamp?: Date;
region?: string,
address?: string,
coordinates?: {
latitude: number
longitude: number
}
[key: string]: any; // Index signature to accept any other properties with their associated types
}
export interface ConfigType {
apiKey?: string;
analyticsLogEndpoint?: string;
analyticsTagEndpoint?: string;
hostProjectName?: string;
trackEvents?: Array<string>;
submissionStrategy?: SubmissionStrategy
showPopUp?: boolean
}
export type SubmissionStrategy = "ONEVENT" | "DEFER"
export type objType = { [key: string]: any }
export type EVENT_TYPES = "CLICK" | "SUBMIT" | "SCROLL" | string

40
index.ts Normal file
View File

@@ -0,0 +1,40 @@
import {
init,
identify,
getEnvironment,
getEnvironmentType,
trackEvent,
trackPageView,
trackError,
enableTracking,
generateAnonymousId,
sendHostProjectName,
getSessionId,
startSession,
} from "./analytics";
document.addEventListener("DOMContentLoaded", function (event) {
const environment = getEnvironment();
console.info("[ANALYTICS] Idenfitied environment: ", environment)
if (!environment || environment === "development") {
console.info("[ANALYTICS] Attempting to auto-load analytics configurations from environment...")
init();
} else {
console.info("[ANALYTICS] Identified non-dev environment, analytics auto load wouldn't be attempted.")
}
});
export {
init,
identify,
getEnvironment,
getEnvironmentType,
trackEvent,
trackPageView,
trackError,
enableTracking,
generateAnonymousId,
sendHostProjectName,
getSessionId,
startSession,
};

48
location.ts Normal file
View File

@@ -0,0 +1,48 @@
import jstz from "jstz";
export function ipLookup() {
fetch('https://extreme-ip-lookup.com/json/')
.then(res => res.json())
.then(response => {
fallbackProcess(response)
})
.catch(() => {
console.log('We could not find your location');
})
}
export function success(position: any, callback: Function) {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
reverseGeocodingWithGoogle(latitude, longitude, callback)
}
export function error() {
console.log("Unable to retrieve your location");
}
function reverseGeocodingWithGoogle(latitude: string, longitude: string, callback: Function) {
fetch(`https://maps.googleapis.com/maps/api/geocode/json?
latlng=${latitude},${longitude}&key={GOOGLE_MAP_KEY}`)
.then(res => res.json())
.then(response => {
callback ? callback(response) : processUserData(response)
})
.catch(status => {
ipLookup()
})
}
function processUserData(response: any) {
console.log(response.results[0].formatted_address);
}
function fallbackProcess(response: any) {
const address: any = document.querySelector('.address')
address.innerText = `${response.city}, ${response.country}`
}
const localTimeRegion = jstz.determine().name();
const localTime = new Date().toLocaleString("en-US", { timeZone: localTimeRegion });
export { localTimeRegion, localTime };

309
package-lock.json generated Normal file
View File

@@ -0,0 +1,309 @@
{
"name": "@armco/analytics",
"version": "0.1.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@armco/analytics",
"version": "0.1.6",
"license": "ISC",
"dependencies": {
"jet-logger": "^1.3.1",
"jquery": "^3.7.0",
"js-cookie": "^3.0.5",
"jstz": "^2.1.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/jquery": "^3.5.16",
"@types/js-cookie": "^3.0.3",
"@types/node": "^20.4.2",
"@types/uuid": "^9.0.2",
"fs-extra": "^11.1.1",
"typescript": "^5.1.6"
},
"peerDependencies": {
"jquery": "^3.7.0"
}
},
"node_modules/@types/fs-extra": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz",
"integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==",
"dev": true,
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/jquery": {
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz",
"integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==",
"dev": true,
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/js-cookie": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz",
"integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==",
"dev": true
},
"node_modules/@types/jsonfile": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz",
"integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "20.4.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
"integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==",
"dev": true
},
"node_modules/@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz",
"integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==",
"dev": true
},
"node_modules/colors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz",
"integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/fs-extra": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
"integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/jet-logger": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jet-logger/-/jet-logger-1.3.1.tgz",
"integrity": "sha512-BSsTm88Y7a+XtXKpZM71qm0ulH+bNI13rR+BzeQStfjpE/6n3fX3FZpKF/WZh52h1e6gEAOjuFlkmdzGBQnwPg==",
"dependencies": {
"colors": "1.3.0"
}
},
"node_modules/jquery": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"engines": {
"node": ">=14"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jstz": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/jstz/-/jstz-2.1.1.tgz",
"integrity": "sha512-8hfl5RD6P7rEeIbzStBz3h4f+BQHfq/ABtoU6gXKQv5OcZhnmrIpG7e1pYaZ8hS9e0mp+bxUj08fnDUbKctYyA==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/typescript": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
}
},
"dependencies": {
"@types/fs-extra": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz",
"integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==",
"dev": true,
"requires": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"@types/jquery": {
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz",
"integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
},
"@types/js-cookie": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz",
"integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==",
"dev": true
},
"@types/jsonfile": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz",
"integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "20.4.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
"integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==",
"dev": true
},
"@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
"@types/uuid": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz",
"integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==",
"dev": true
},
"colors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz",
"integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw=="
},
"fs-extra": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
"integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"jet-logger": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jet-logger/-/jet-logger-1.3.1.tgz",
"integrity": "sha512-BSsTm88Y7a+XtXKpZM71qm0ulH+bNI13rR+BzeQStfjpE/6n3fX3FZpKF/WZh52h1e6gEAOjuFlkmdzGBQnwPg==",
"requires": {
"colors": "1.3.0"
}
},
"jquery": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
},
"js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"jstz": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/jstz/-/jstz-2.1.1.tgz",
"integrity": "sha512-8hfl5RD6P7rEeIbzStBz3h4f+BQHfq/ABtoU6gXKQv5OcZhnmrIpG7e1pYaZ8hS9e0mp+bxUj08fnDUbKctYyA=="
},
"typescript": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"dev": true
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
}
}
}

58
package.json Normal file
View File

@@ -0,0 +1,58 @@
{
"name": "@armco/analytics",
"version": "0.2.8",
"description": "Browser Based Analytics interceptor for configured events",
"main": "index.js",
"type": "module",
"scripts": {
"build": "npx ts-node build.js",
"lint": "npx eslint --ext .ts src/",
"lint:tests": "npx eslint --ext .ts spec/",
"start": "node ./dist --env=production",
"dev": "nodemon",
"test": "nodemon --config ./spec/nodemon.json",
"test:no-reloading": "npx ts-node --files -r tsconfig-paths/register ./spec",
"publish:local": "./publish-local.sh",
"publish:sh": "./publish.sh",
"publish:sh:minor": "./publish.sh minor"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ReStruct-Corporate-Advantage/analytics.git"
},
"keywords": [
"analytics",
"browser",
"configurable",
"insights",
"automated"
],
"author": "mohit.nagar@armco.tech",
"license": "ISC",
"bugs": {
"url": "https://github.com/ReStruct-Corporate-Advantage/analytics/issues"
},
"homepage": "https://github.com/ReStruct-Corporate-Advantage/analytics#readme",
"files": [
"*"
],
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/jquery": "^3.5.16",
"@types/js-cookie": "^3.0.3",
"@types/node": "^20.4.2",
"@types/uuid": "^9.0.2",
"fs-extra": "^11.1.1",
"typescript": "^5.1.6"
},
"dependencies": {
"jet-logger": "^1.3.1",
"jquery": "^3.7.0",
"js-cookie": "^3.0.5",
"jstz": "^2.1.1",
"uuid": "^9.0.0"
},
"peerDependencies": {
"jquery": "^3.7.0"
}
}

8
publish-local.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
semver=${1:-patch}
set -e
npm run build
cd dist
npm pack --pack-destination ~/__Projects__/Common

9
publish.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
semver=${1:-patch}
npm run build
cd dist
npm --no-git-tag-version version ${semver}
npm publish --access public

126
session.ts Normal file
View File

@@ -0,0 +1,126 @@
import Cookies from 'js-cookie';
import {v4 as uuidv4} from "uuid";
const SESSION_COOKIE_NAME = 'ar-session-id';
const SESSION_EXPIRATION_TIME = 30; // In minutes
let localStorageTimeout: NodeJS.Timeout;
// Function to generate a new session ID
function generateSessionId() {
return uuidv4();
}
// Function to start a new session and generate a session ID
export function startSession() {
const sessionId = generateSessionId();
const expirationDate = new Date();
// Generate a unique identifier for this tab if it doesn't already have one
let tabId = sessionStorage.getItem('tabId');
if (!tabId) {
const timestamp = expirationDate.getTime();
tabId = `${uuidv4()}-${timestamp}`;
sessionStorage.setItem('tabId', tabId);
}
// Combine the tab ID and session ID to create a unique cookie name for this tab
const cookieName = `${SESSION_COOKIE_NAME}-${tabId}`;
refreshSessionId(sessionId, cookieName);
return sessionId;
}
// Function to refresh the session ID expiration time
function refreshSessionId(sessionId: string, cookieName: string) {
const expirationDate = new Date();
expirationDate.setMinutes(expirationDate.getMinutes() + SESSION_EXPIRATION_TIME);
// Refresh the session ID expiration time in cookies or localStorage
try {
Cookies.set(cookieName, sessionId, { expires: expirationDate });
} catch (error) {
clearTimeout(localStorageTimeout);
localStorageTimeout = setTimeout(() => localStorage.removeItem(cookieName), SESSION_EXPIRATION_TIME * 1000);
}
}
// Function to get the current session ID (if it exists) and refresh the cookie expiration time
export function getSessionId() {
let sessionId: string | null | undefined;
// Get the tab ID from sessionStorage
const tabId = sessionStorage.getItem('tabId');
if (!tabId) {
// If there's no tab ID, start a new session
return startSession();
}
// Combine the tab ID and session ID to create the cookie name
const cookieName = `${SESSION_COOKIE_NAME}-${tabId}`;
sessionId = Cookies.get(cookieName);
// If the session ID is not found in cookies, check localStorage
if (!sessionId) {
sessionId = localStorage.getItem(cookieName);
}
if (!sessionId) {
return startSession();
}
refreshSessionId(sessionId, cookieName);
return sessionId;
}
export function extendSession() {
// Get the tab ID from sessionStorage
const tabId = sessionStorage.getItem('tabId');
if (!tabId) {
// If there's no tab ID, there's no session to extend
return;
}
// Combine the tab ID and session ID to create the cookie name
const cookieName = `${SESSION_COOKIE_NAME}-${tabId}`;
let sessionId: string | null | undefined = Cookies.get(cookieName);
// If the session ID is not found in cookies, check localStorage
if (!sessionId) {
sessionId = localStorage.getItem(cookieName);
}
if (sessionId) {
refreshSessionId(sessionId, cookieName);
}
}
// Helper function to remove the session cookie
export function terminateSession(): void {
// Get the tab ID from sessionStorage
const tabId = sessionStorage.getItem('tabId');
if (!tabId) {
// If there's no tab ID, there's nothing to remove
return;
}
// Combine the tab ID and session ID to create the cookie name
const cookieName = `${SESSION_COOKIE_NAME}-${tabId}`;
// Use Cookies if available
if (typeof window !== 'undefined' && window.Cookies) {
Cookies.remove(cookieName);
} else {
// Fallback to localStorage (blocked Cookies)
localStorage.removeItem(cookieName);
}
}
// // Function to handle browser quit event
// window.addEventListener('beforeunload', () => {
// // Logout the session on browser quit
// logout();
// });

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"declaration": true,
"declarationDir": "dist",
"target": "es6",
"module": "esnext",
"moduleResolution": "node",
"outDir": "./dist",
"strict": true,
"baseUrl": "./",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@src/*": [
"src/*"
]
},
"useUnknownInCatchVariables": false
},
"include": [
"./**/*.ts",
"build.js"
],
"exclude": [
"src/public/",
"dist"
]
}

11
tsconfig.prod.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"removeComments": true
},
"exclude": [
"spec",
"build.ts"
]
}