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
Some checks failed
armco-org/analytics/pipeline/head There was a failure building this commit
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user