Added init loggers && updated to accept endpoints for multiple environment
Some checks failed
armco-org/analytics/pipeline/head There was a failure building this commit

This commit is contained in:
2025-12-21 15:07:25 +05:30
parent 9a9dd95647
commit 08b3137272
8 changed files with 281 additions and 11 deletions

View File

@@ -33,6 +33,8 @@ import {
getTimestamp,
deepMerge,
} from "../utils/helpers";
import { resolveEndpoint, detectEnvironment } from "../utils/config-loader";
import type { EndpointConfig } from "./types";
/**
* Analytics builder class
@@ -208,13 +210,26 @@ export class Analytics implements IAnalytics {
throw new InitializationError("Analytics already initialized");
}
this.logger.info("Loading Configuration");
// Check Do Not Track
this.logger.info("Check if Analytics enabled");
if (this.config.respectDoNotTrack && isDoNotTrackEnabled()) {
this.logger.warn("Do Not Track is enabled, analytics will be disabled");
this.enabled = false;
return;
}
this.logger.info("Configuration loaded");
// Log environment detection
const currentEnv = this.config.environment || detectEnvironment();
this.logger.info(`Identified environment: ${currentEnv}`);
if (currentEnv !== "development" && currentEnv !== "local") {
this.logger.info("Identified non-dev environment, analytics auto load wouldn't be attempted.");
}
// Create plugin context
const context: PluginContext = {
config: this.config,
@@ -225,6 +240,14 @@ export class Analytics implements IAnalytics {
getUserId: this.getUserId.bind(this),
};
// Show consent popup if configured
if (this.config.showConsentPopup) {
this.logger.info("Display Tracker Popup");
}
// Hook event trackers
this.logger.info("Hook Event Trackers");
// Initialize all plugins
for (const plugin of this.plugins) {
try {
@@ -237,11 +260,20 @@ export class Analytics implements IAnalytics {
// Set up flush interval for deferred submission
if (this.config.submissionStrategy === "DEFER") {
this.logger.info('Hook Handlers to flush events (use when submissionStrategy is configured as "DEFER"');
this.flushInterval = setInterval(() => {
this.flush();
}, this.config.flushInterval);
}
// Location detection
if (this.config.enableLocation) {
this.logger.info("Find User Location Details");
}
// Session and user initialization
this.logger.info("Initiate Session and Anonymous User ID");
// Set up browser-only unload handlers
if (getEnvironmentType() === "browser") {
window.addEventListener("beforeunload", () => {
@@ -448,14 +480,55 @@ export class Analytics implements IAnalytics {
/**
* Get analytics endpoint
* Returns the resolved endpoint, handling both string and multi-endpoint configs
*/
private getEndpoint(): string {
if (this.config.endpoint) {
// Use resolved endpoint if available (set during async initialization)
if (this.config.resolvedEndpoint) {
return this.config.resolvedEndpoint;
}
// Handle string endpoint (backward compatible)
if (typeof this.config.endpoint === "string") {
return this.config.endpoint;
}
// Handle object endpoint (multi-endpoint config) - synchronous fallback
if (this.config.endpoint && typeof this.config.endpoint === "object") {
const endpoints = this.config.endpoint as EndpointConfig;
const currentEnv = this.config.environment || detectEnvironment();
if (endpoints[currentEnv]) {
return endpoints[currentEnv];
}
// Try fallback environments
const fallbackOrder = ["development", "local", "staging", "production"];
for (const fallbackEnv of fallbackOrder) {
if (endpoints[fallbackEnv]) {
return endpoints[fallbackEnv];
}
}
}
// Default Armco endpoint if apiKey is provided
return "https://telemetry.armco.dev/events/add";
if (this.config.apiKey) {
return "https://telemetry.armco.dev/events/add";
}
// Final fallback
return "http://localhost:5001/events/add";
}
/**
* Resolve endpoint asynchronously with health check
* Call this before init() for full endpoint resolution with health checking
*/
async resolveEndpointAsync(): Promise<void> {
this.logger.info("Resolving endpoint with health check...");
const resolved = await resolveEndpoint(this.config);
this.config.resolvedEndpoint = resolved;
this.logger.info(`Resolved endpoint: ${resolved}`);
}
/**

View File

@@ -7,6 +7,16 @@
*/
export type Environment = "browser" | "node" | "unknown";
/**
* Runtime environment names for endpoint configuration
*/
export type EnvironmentName = "development" | "staging" | "production" | "test" | "local" | string;
/**
* Multi-endpoint configuration mapping environment names to URLs
*/
export type EndpointConfig = Record<EnvironmentName, string>;
/**
* Submission strategy for events
*/
@@ -105,8 +115,22 @@ export interface LocationData {
*/
export interface AnalyticsConfig {
apiKey?: string;
endpoint?: string;
/**
* Single endpoint URL (backward compatible) or multi-endpoint config
* When object: keys are environment names, values are URLs
* Example: { "production": "https://telemetry.armco.dev/events/add", "development": "http://localhost:5001/events/add" }
*/
endpoint?: string | EndpointConfig;
/**
* Resolved endpoint URL (set internally after endpoint resolution)
*/
resolvedEndpoint?: string;
updateEndpoint?: string; // Endpoint for updating anonymous events with user identity
/**
* Current environment name to use for endpoint resolution
* If not set, will be auto-detected from NODE_ENV or window location
*/
environment?: EnvironmentName;
hostProjectName?: string;
trackEvents?: string[];
submissionStrategy?: SubmissionStrategy;

View File

@@ -87,7 +87,13 @@ export {
deepMerge,
} from "./utils/helpers";
export { loadConfigFromFile, loadConfig } from "./utils/config-loader";
export {
loadConfigFromFile,
loadConfig,
detectEnvironment,
checkEndpointHealth,
resolveEndpoint
} from "./utils/config-loader";
// Create helper function
import { AnalyticsBuilder as Builder } from "./core/analytics";

View File

@@ -35,8 +35,14 @@ export class ClickTrackingPlugin implements Plugin {
}
this.context = context;
this.logger.info("Attaching Click handlers");
this.attachHandlers();
this.logger.info("Click tracking initialized");
// Count trackable elements
const trackableElements = document.querySelectorAll(TRACKED_ELEMENTS.join(", "));
this.logger.info(`Found ${trackableElements.length} items that can be clicked!`);
this.logger.info("Dynamically added elements will be added to this list.");
this.logger.info("Click handlers Attached");
}
/**

View File

@@ -27,6 +27,7 @@ export class PageTrackingPlugin implements Plugin {
this.attachHandlers();
// Track initial page view
this.logger.info("Logging page load");
this.trackCurrentPage();
this.logger.info("Page tracking initialized");

View File

@@ -29,6 +29,12 @@ export class UserPlugin implements Plugin {
if (!this.user) {
this.generateAnonymousId();
}
// Log user tracking info
const userId = this.getUserId();
if (userId) {
this.logger.info(`Tracking User as ${userId}`);
}
}
/**
@@ -198,8 +204,11 @@ export class UserPlugin implements Plugin {
// If no explicit update endpoint, use default based on config
if (config.apiKey) {
updateEndpoint = "https://telemetry.armco.dev/events/tag";
} else if (config.endpoint) {
// Derive update endpoint from main endpoint
} else if (config.resolvedEndpoint) {
// Derive update endpoint from resolved endpoint
updateEndpoint = config.resolvedEndpoint.replace("/add", "/tag");
} else if (typeof config.endpoint === "string") {
// Derive update endpoint from main endpoint (string only)
updateEndpoint = config.endpoint.replace("/add", "/tag");
} else {
this.logger.warn("No update endpoint configured, skipping event tagging");
@@ -212,7 +221,7 @@ export class UserPlugin implements Plugin {
`Updating anonymous events (${anonymousId}) with user identity (${email})`
);
const response = await transport.update(updateEndpoint, {
const response = await transport.update!(updateEndpoint, {
email,
anonymousId,
});

View File

@@ -9,11 +9,12 @@
* TODO(Config): Add support for environment-specific configs (analyticsrc.dev.json, analyticsrc.prod.json)
*/
import type { AnalyticsConfig } from "../core/types";
import { getEnvironmentType } from "./helpers";
import type { AnalyticsConfig, EndpointConfig, EnvironmentName } from "../core/types";
import { getEnvironmentType, getEnvironment } from "./helpers";
import { getLogger } from "./logging";
const CONFIG_FILE_NAME = "analyticsrc";
const DEFAULT_FALLBACK_ENDPOINT = "http://localhost:5001/events/add";
const logger = getLogger();
/**
@@ -103,3 +104,153 @@ export async function loadConfig(
...overrides,
};
}
/**
* Detect current environment name from various sources
*/
export function detectEnvironment(): EnvironmentName {
const envType = getEnvironmentType();
if (envType === "node") {
// Check NODE_ENV
const nodeEnv = process.env.NODE_ENV?.toLowerCase();
if (nodeEnv) {
return nodeEnv as EnvironmentName;
}
return "development";
}
if (envType === "browser") {
// Detect from hostname
const hostname = window.location.hostname;
if (hostname === "localhost" || hostname === "127.0.0.1") {
return "development";
}
if (hostname.includes("staging") || hostname.includes("stage")) {
return "staging";
}
if (hostname.includes("test") || hostname.includes("qa")) {
return "test";
}
// Default to production for non-local environments
return "production";
}
return "development";
}
/**
* Check if an endpoint URL is reachable
* Returns true if endpoint responds, false otherwise
*/
export async function checkEndpointHealth(url: string): Promise<boolean> {
try {
const envType = getEnvironmentType();
if (envType === "node") {
// Node.js: Use fetch with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url.replace("/events/add", "/health"), {
method: "GET",
signal: controller.signal,
});
clearTimeout(timeout);
return response.ok || response.status === 404; // 404 is acceptable - endpoint exists
} catch {
clearTimeout(timeout);
return false;
}
} else if (envType === "browser") {
// Browser: Use fetch with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url.replace("/events/add", "/health"), {
method: "GET",
mode: "no-cors", // Avoid CORS issues for health check
signal: controller.signal,
});
clearTimeout(timeout);
// In no-cors mode, we can't read response but if it doesn't throw, endpoint is reachable
return true;
} catch {
clearTimeout(timeout);
return false;
}
}
return true; // Default to true for unknown environments
} catch {
return false;
}
}
/**
* Resolve endpoint URL from config based on environment
* Performs health check and falls back to localhost if endpoint is unreachable
*/
export async function resolveEndpoint(
config: Partial<AnalyticsConfig>
): Promise<string> {
const currentEnv = config.environment || detectEnvironment();
logger.info(`Identified environment: ${currentEnv}`);
// If endpoint is a string (backward compatible), return as-is
if (typeof config.endpoint === "string") {
logger.debug(`Using single endpoint: ${config.endpoint}`);
return config.endpoint;
}
// If endpoint is an object (multi-endpoint config)
if (config.endpoint && typeof config.endpoint === "object") {
const endpoints = config.endpoint as EndpointConfig;
const envEndpoint = endpoints[currentEnv];
if (envEndpoint) {
logger.info(`Found endpoint for environment '${currentEnv}': ${envEndpoint}`);
// Check if endpoint is reachable
const isHealthy = await checkEndpointHealth(envEndpoint);
if (isHealthy) {
logger.info(`Endpoint ${envEndpoint} is reachable`);
return envEndpoint;
} else {
logger.warn(`Endpoint ${envEndpoint} is not reachable, falling back to ${DEFAULT_FALLBACK_ENDPOINT}`);
return DEFAULT_FALLBACK_ENDPOINT;
}
} else {
// Try to find a fallback in order of preference
const fallbackOrder: EnvironmentName[] = ["development", "local", "staging", "production"];
for (const fallbackEnv of fallbackOrder) {
if (endpoints[fallbackEnv]) {
logger.warn(`No endpoint configured for '${currentEnv}', using '${fallbackEnv}' endpoint: ${endpoints[fallbackEnv]}`);
return endpoints[fallbackEnv];
}
}
// No endpoint found, use default fallback
logger.warn(`No endpoint found in configuration, using fallback: ${DEFAULT_FALLBACK_ENDPOINT}`);
return DEFAULT_FALLBACK_ENDPOINT;
}
}
// No endpoint configured at all, use default
if (config.apiKey) {
const defaultEndpoint = "https://telemetry.armco.dev/events/add";
logger.info(`Using default Armco endpoint: ${defaultEndpoint}`);
return defaultEndpoint;
}
logger.warn(`No endpoint configured, using fallback: ${DEFAULT_FALLBACK_ENDPOINT}`);
return DEFAULT_FALLBACK_ENDPOINT;
}

View File

@@ -11,7 +11,7 @@ export class Logger {
private level: LogLevel;
private prefix: string;
constructor(level: LogLevel = "info", prefix = "[Analytics]") {
constructor(level: LogLevel = "info", prefix = "[ANALYTICS]") {
this.level = level;
this.prefix = prefix;
}