First Commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
6
.npmignore
Normal file
6
.npmignore
Normal file
@@ -0,0 +1,6 @@
|
||||
tsconfig*
|
||||
package-lock.json
|
||||
build.ts
|
||||
build.js
|
||||
node_modules
|
||||
index.ts
|
||||
618
analytics.ts
Normal file
618
analytics.ts
Normal 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
57
build.js
Normal 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
3
constants.ts
Normal 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
55
flush.ts
Normal 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
14
global-modules.d.ts
vendored
Normal 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
3
helper.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isArClient(name: string | null): boolean {
|
||||
return !!(name && name.startsWith("@armco"));
|
||||
}
|
||||
32
index.interface.ts
Normal file
32
index.interface.ts
Normal 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
40
index.ts
Normal 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
48
location.ts
Normal 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
309
package-lock.json
generated
Normal 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
58
package.json
Normal 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
8
publish-local.sh
Executable 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
9
publish.sh
Executable 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
126
session.ts
Normal 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
29
tsconfig.json
Normal 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
11
tsconfig.prod.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": false,
|
||||
"removeComments": true
|
||||
},
|
||||
"exclude": [
|
||||
"spec",
|
||||
"build.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user