From 207224356163053761fe8485302b73eaaa5ea6db Mon Sep 17 00:00:00 2001 From: mohiit1502 Date: Wed, 4 Sep 2024 21:35:55 +0530 Subject: [PATCH] First Commit --- .env | 1 + .gitignore | 2 + .npmignore | 6 + analytics.ts | 618 ++++++++++++++++++++++++++++++++++++++++++++ build.js | 57 ++++ constants.ts | 3 + flush.ts | 55 ++++ global-modules.d.ts | 14 + helper.ts | 3 + index.interface.ts | 32 +++ index.ts | 40 +++ location.ts | 48 ++++ package-lock.json | 309 ++++++++++++++++++++++ package.json | 58 +++++ publish-local.sh | 8 + publish.sh | 9 + session.ts | 126 +++++++++ tsconfig.json | 29 +++ tsconfig.prod.json | 11 + 19 files changed, 1429 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 analytics.ts create mode 100644 build.js create mode 100644 constants.ts create mode 100644 flush.ts create mode 100644 global-modules.d.ts create mode 100644 helper.ts create mode 100644 index.interface.ts create mode 100644 index.ts create mode 100644 location.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 publish-local.sh create mode 100755 publish.sh create mode 100644 session.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.prod.json diff --git a/.env b/.env new file mode 100644 index 0000000..995fca4 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NODE_ENV=production \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..24e2427 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +tsconfig* +package-lock.json +build.ts +build.js +node_modules +index.ts \ No newline at end of file diff --git a/analytics.ts b/analytics.ts new file mode 100644 index 0000000..5609229 --- /dev/null +++ b/analytics.ts @@ -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 { + + // 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 /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 { + 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 { + 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 { + 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 = ` +
+

We use cookies to collect and analyze data to improve our website. By clicking "Accept," you consent to the use of cookies.

+ Got it! +
+ `; + + $('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, +}; diff --git a/build.js b/build.js new file mode 100644 index 0000000..d6788b7 --- /dev/null +++ b/build.js @@ -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(); + }); + }); +} diff --git a/constants.ts b/constants.ts new file mode 100644 index 0000000..96c8dc3 --- /dev/null +++ b/constants.ts @@ -0,0 +1,3 @@ +export const ARMCO_SERVER = "https://telemetry.armco.tech/events/"; +export const ADD = "add"; +export const TAG = "tag" diff --git a/flush.ts b/flush.ts new file mode 100644 index 0000000..405dab8 --- /dev/null +++ b/flush.ts @@ -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); + } +} \ No newline at end of file diff --git a/global-modules.d.ts b/global-modules.d.ts new file mode 100644 index 0000000..98d75ed --- /dev/null +++ b/global-modules.d.ts @@ -0,0 +1,14 @@ +import analytics from './index'; + +declare global { + namespace NodeJS { + interface Global { + analytics: analytics; + } + } + interface Window { + analytics: analytics; + } +} + +export {}; \ No newline at end of file diff --git a/helper.ts b/helper.ts new file mode 100644 index 0000000..2b58240 --- /dev/null +++ b/helper.ts @@ -0,0 +1,3 @@ +export function isArClient(name: string | null): boolean { + return !!(name && name.startsWith("@armco")); +} \ No newline at end of file diff --git a/index.interface.ts b/index.interface.ts new file mode 100644 index 0000000..7b1c1d2 --- /dev/null +++ b/index.interface.ts @@ -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; + submissionStrategy?: SubmissionStrategy + showPopUp?: boolean +} + +export type SubmissionStrategy = "ONEVENT" | "DEFER" +export type objType = { [key: string]: any } +export type EVENT_TYPES = "CLICK" | "SUBMIT" | "SCROLL" | string + diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..ec69781 --- /dev/null +++ b/index.ts @@ -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, +}; diff --git a/location.ts b/location.ts new file mode 100644 index 0000000..afb7239 --- /dev/null +++ b/location.ts @@ -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 }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..186c7ea --- /dev/null +++ b/package-lock.json @@ -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==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..998ed8f --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/publish-local.sh b/publish-local.sh new file mode 100755 index 0000000..daf915a --- /dev/null +++ b/publish-local.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +semver=${1:-patch} + +set -e +npm run build +cd dist +npm pack --pack-destination ~/__Projects__/Common \ No newline at end of file diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..8687652 --- /dev/null +++ b/publish.sh @@ -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 \ No newline at end of file diff --git a/session.ts b/session.ts new file mode 100644 index 0000000..d27bba4 --- /dev/null +++ b/session.ts @@ -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(); +// }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4d0ba6f --- /dev/null +++ b/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/tsconfig.prod.json b/tsconfig.prod.json new file mode 100644 index 0000000..85dd1b8 --- /dev/null +++ b/tsconfig.prod.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "removeComments": true + }, + "exclude": [ + "spec", + "build.ts" + ] +}