Auto-commit 2026-04-27 12:34

This commit is contained in:
2026-04-27 12:34:30 -04:00
parent bef1d7f829
commit 9e3a54f508
12 changed files with 1025 additions and 5 deletions

View File

@@ -0,0 +1,93 @@
import { createMixpanelService, type MixpanelConfig } from "./mixpanel-service";
import { createGA4Service, type GA4Config } from "./ga4-service";
import { createStripeService, type StripeDashboardConfig } from "./stripe-service";
export interface AnalyticsConfig {
mixpanel: MixpanelConfig;
ga4: GA4Config;
stripe: StripeDashboardConfig;
}
export class AnalyticsConfigManager {
private config: AnalyticsConfig;
private mixpanelService?: ReturnType<typeof createMixpanelService>;
private ga4Service?: ReturnType<typeof createGA4Service>;
private stripeService?: ReturnType<typeof createStripeService>;
constructor(config: AnalyticsConfig) {
this.config = config;
}
initialize(): void {
// Initialize Mixpanel
this.mixpanelService = createMixpanelService(this.config.mixpanel);
// Initialize GA4
this.ga4Service = createGA4Service(this.config.ga4);
this.ga4Service.initialize();
// Initialize Stripe
this.stripeService = createStripeService(this.config.stripe);
this.stripeService.initialize();
}
getMixpanel(): ReturnType<typeof createMixpanelService> {
if (!this.mixpanelService) {
throw new Error("Analytics not initialized. Call initialize() first.");
}
return this.mixpanelService;
}
getGA4(): ReturnType<typeof createGA4Service> {
if (!this.ga4Service) {
throw new Error("Analytics not initialized. Call initialize() first.");
}
return this.ga4Service;
}
getStripe(): ReturnType<typeof createStripeService> {
if (!this.stripeService) {
throw new Error("Analytics not initialized. Call initialize() first.");
}
return this.stripeService;
}
getConfig(): AnalyticsConfig {
return this.config;
}
}
export const loadAnalyticsConfig = (): AnalyticsConfig => {
return {
mixpanel: {
projectToken: import.meta.env.VITE_MIXPANEL_PROJECT_TOKEN || "",
options: {
debug: import.meta.env.VITE_MIXPANEL_DEBUG === "true",
track_pageview: true,
persistence: "localStorage",
},
},
ga4: {
measurementId: import.meta.env.VITE_GA4_MEASUREMENT_ID || "",
options: {
debug: import.meta.env.VITE_GA4_DEBUG === "true",
autoTrackPageViews: true,
autoTrackScrolls: true,
autoTrackOutboundLinks: true,
},
},
stripe: {
publishableKey: import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || "",
secretKey: import.meta.env.VITE_STRIPE_SECRET_KEY || "",
webhookSecret: import.meta.env.VITE_STRIPE_WEBHOOK_SECRET || "",
apiVersion: "2024-12-18.acacia",
},
};
};
export const createAnalyticsManager = (): AnalyticsConfigManager => {
const config = loadAnalyticsConfig();
const manager = new AnalyticsConfigManager(config);
manager.initialize();
return manager;
};

View File

@@ -0,0 +1,78 @@
export interface GA4LoadOptions {
debug?: boolean;
autoTrackPageViews?: boolean;
autoTrackScrolls?: boolean;
autoTrackOutboundLinks?: boolean;
}
export const loadGA4 = async (
measurementId: string,
options: GA4LoadOptions = {}
): Promise<void> => {
const {
debug = false,
autoTrackPageViews = true,
autoTrackScrolls = true,
autoTrackOutboundLinks = false,
} = options;
// Load GA4 script
await new Promise<void>((resolve) => {
const script = document.createElement("script");
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
script.onload = () => resolve();
document.head.appendChild(script);
});
// Initialize gtag
window.dataLayer = window.dataLayer || [];
(window as any).gtag = function (...args: unknown[]) {
(window as any).dataLayer.push(args);
};
// Configure GA4
(window as any).gtag("config", measurementId, {
debug_mode: debug,
send_page_view: autoTrackPageViews,
});
// Auto-track scrolls
if (autoTrackScrolls) {
const scrollDepth = [25, 50, 75, 100];
scrollDepth.forEach((threshold) => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
(window as any).gtag("event", "scroll", {
scroll_percentage: threshold,
});
observer.unobserve(entry.target);
}
});
},
{ threshold: threshold / 100 }
);
observer.observe(document.body);
});
}
// Auto-track outbound links
if (autoTrackOutboundLinks) {
document.addEventListener("click", (e) => {
const target = e.target as HTMLAnchorElement;
if (target.tagName === "A" && target.href) {
const currentHost = window.location.hostname;
const linkHost = new URL(target.href).hostname;
if (linkHost !== currentHost) {
(window as any).gtag("event", "click", {
event_category: "outbound_link",
event_label: target.href,
value: 1,
});
}
}
});
}
};

View File

@@ -0,0 +1,139 @@
import { loadGA4 } from "./ga4-loader";
type GA4Event =
| "session_start"
| "page_view"
| "scroll"
| "click"
| "user_signup"
| "user_login"
| "user_logout"
| "project_create"
| "project_update"
| "project_delete"
| "screenplay_create"
| "screenplay_update"
| "screenplay_export"
| "begin_checkout"
| "add_to_cart"
| "add_to_wishlist"
| "view_item"
| "view_item_list"
| "select_item"
| "purchase"
| "refund"
| "sign_up"
| "lead"
| "search";
export interface GA4Config {
measurementId: string;
apiSecret?: string;
streamId?: string;
options?: {
debug?: boolean;
autoTrackPageViews?: boolean;
autoTrackScrolls?: boolean;
autoTrackOutboundLinks?: boolean;
};
}
export interface GA4EcommerceItem {
item_id: string;
item_name: string;
item_category?: string;
price: number;
quantity: number;
currency?: string;
}
export interface GA4PurchaseEvent {
transaction_id: string;
value: number;
currency: string;
tax?: number;
shipping?: number;
items: GA4EcommerceItem[];
}
export class GA4Service {
private measurementId: string;
private config: GA4Config;
private initialized: boolean = false;
constructor(config: GA4Config) {
this.measurementId = config.measurementId;
this.config = config;
}
async initialize(): Promise<void> {
await loadGA4(this.measurementId, this.config.options);
this.initialized = true;
}
track(event: GA4Event, params?: Record<string, unknown>): void {
if (!this.initialized) {
console.warn("GA4 not initialized");
return;
}
(window as any).gtag?.("event", event, params);
}
trackPageView(pageLocation?: string, pagePath?: string): void {
this.track("page_view", {
page_location: pageLocation || window.location.href,
page_path: pagePath || window.location.pathname,
page_title: document.title,
});
}
trackEcommerce(event: "purchase" | "refund", data: GA4PurchaseEvent): void {
this.track(event, {
transaction_id: data.transaction_id,
value: data.value,
currency: data.currency,
tax: data.tax,
shipping: data.shipping,
items: data.items,
});
}
trackAddToCart(item: GA4EcommerceItem): void {
this.track("add_to_cart", {
items: [item],
});
}
trackBeginCheckout(items: GA4EcommerceItem[], value: number): void {
this.track("begin_checkout", {
items,
value,
currency: items[0]?.currency || "USD",
});
}
trackViewItemList(items: GA4EcommerceItem[], itemListName: string): void {
this.track("view_item_list", {
items,
item_list_name: itemListName,
});
}
trackSelectItem(item: GA4EcommerceItem, itemListName: string): void {
this.track("select_item", {
items: [item],
item_list_name: itemListName,
});
}
setUserProperty(userId: string, userProperties?: Record<string, string>): void {
(window as any).gtag?.("set", "user_properties", {
user_id: userId,
...userProperties,
});
}
}
export const createGA4Service = (config: GA4Config): GA4Service => {
return new GA4Service(config);
};

View File

@@ -3,3 +3,7 @@ export * from "./slack-alerts";
export * from "./report-generator";
export * from "./cohort-analysis";
export * from "./nps-service";
export * from "./mixpanel-service";
export * from "./ga4-service";
export * from "./ga4-loader";
export * from "./stripe-service";

View File

@@ -0,0 +1,84 @@
import mixpanel, { type MixpanelInstance } from "mixpanel-browser";
type MixpanelEvent =
| "user_signedup"
| "user_signedin"
| "user_signedout"
| "project_created"
| "project_updated"
| "project_deleted"
| "screenplay_created"
| "screenplay_updated"
| "screenplay exported"
| "subscription_started"
| "subscription_renewed"
| "subscription_cancelled"
| "payment_succeeded"
| "payment_failed"
| "invitation_sent"
| "collaboration_started"
| "collaboration_updated"
| "export_completed"
| "import_completed";
export interface MixpanelConfig {
projectToken: string;
options?: {
debug?: boolean;
track_pageview?: boolean;
persistence?: "localStorage" | "cookie" | "memory";
loaded?: (mixpanel: MixpanelInstance) => void;
};
}
export class MixpanelService {
private mixpanel: MixpanelInstance;
private initialized: boolean = false;
constructor(config: MixpanelConfig) {
this.mixpanel = mixpanel.init(config.projectToken, config.options);
this.initialized = true;
}
track(event: MixpanelEvent, properties?: Record<string, unknown>): void {
if (!this.initialized) {
console.warn("Mixpanel not initialized");
return;
}
this.mixpanel.track(event, properties);
}
identify(userId: string, properties?: Record<string, unknown>): void {
this.mixpanel.identify(userId);
if (properties) {
this.mixpanel.people.set(userId, properties);
}
}
alias(distinctId: string, alias: string): void {
this.mixpanel.alias(alias, distinctId);
}
reset(): void {
this.mixpanel.reset();
}
getDistinctId(): string {
return this.mixpanel.get_distinct_id();
}
group(groupType: string, groupId: string): void {
this.mixpanel.group(groupType, groupId);
}
trackWithTimestamp(event: MixpanelEvent, timestamp: Date, properties?: Record<string, unknown>): void {
this.mixpanel.track(event, {
...properties,
timestamp: timestamp.toISOString(),
});
}
}
export const createMixpanelService = (config: MixpanelConfig): MixpanelService => {
return new MixpanelService(config);
};

View File

@@ -0,0 +1,145 @@
import Stripe from "stripe";
export interface StripeWebhookConfig {
secret: string;
apiVersion?: string;
}
export interface StripeDashboardConfig {
publishableKey: string;
secretKey: string;
webhookSecret: string;
apiVersion?: string;
}
type StripeEvent =
| "customer.created"
| "customer.updated"
| "customer.deleted"
| "subscription.created"
| "subscription.updated"
| "subscription.deleted"
| "invoice.created"
| "invoice.payment_succeeded"
| "invoice.payment_failed"
| "payment_intent.created"
| "payment_intent.succeeded"
| "payment_intent.payment_failed"
| "checkout.session.completed"
| "product.created"
| "product.updated"
| "price.created"
| "price.updated";
export class StripeService {
private stripe: Stripe;
private webhookSecret: string;
private initialized: boolean = false;
constructor(config: StripeDashboardConfig) {
this.stripe = new Stripe(config.secretKey, {
apiVersion: config.apiVersion || "2024-12-18.acacia",
});
this.webhookSecret = config.webhookSecret;
this.initialized = true;
}
async initialize(): Promise<void> {
// Verify connection by fetching account
const account = await this.stripe.account.retrieve();
this.initialized = !!account.id;
}
async createCustomer(email: string, name?: string, metadata?: Record<string, string>): Promise<Stripe.Customer> {
return await this.stripe.customers.create({
email,
name,
metadata,
});
}
async createSubscription(customerId: string, priceId: string): Promise<Stripe.Subscription> {
return await this.stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
});
}
async createCheckoutSession(
customerId: string,
priceId: string,
successUrl: string,
cancelUrl: string
): Promise<Stripe.Checkout.Session> {
return await this.stripe.checkout.sessions.create({
customer: customerId,
line_items: [{ price: priceId, quantity: 1 }],
mode: "subscription",
success_url: successUrl,
cancel_url: cancelUrl,
});
}
async verifyWebhook(payload: Buffer, signature: string): Promise<Stripe.Event> {
return await this.stripe.webhooks.constructEventAsync(payload, signature, this.webhookSecret);
}
handleWebhookEvent(event: Stripe.Event): void {
switch (event.type) {
case "customer.created":
console.log("Customer created:", (event.data.object as Stripe.Customer).email);
break;
case "subscription.created":
console.log("Subscription created:", (event.data.object as Stripe.Subscription).id);
break;
case "invoice.payment_succeeded":
console.log("Payment succeeded:", (event.data.object as Stripe.Invoice).id);
break;
case "invoice.payment_failed":
console.log("Payment failed:", (event.data.object as Stripe.Invoice).id);
break;
case "payment_intent.succeeded":
console.log("Payment intent succeeded:", (event.data.object as Stripe.PaymentIntent).id);
break;
case "payment_intent.payment_failed":
console.log("Payment intent failed:", (event.data.object as Stripe.PaymentIntent).id);
break;
case "checkout.session.completed":
console.log("Checkout session completed:", (event.data.object as Stripe.Checkout.Session).id);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
}
async listCustomers(limit?: number): Promise<Stripe.ApiList<Stripe.Customer>> {
return await this.stripe.customers.list({ limit: limit || 10 });
}
async getCustomer(customerId: string): Promise<Stripe.Customer> {
return await this.stripe.customers.retrieve(customerId);
}
async updateCustomer(
customerId: string,
updates: { email?: string; name?: string; metadata?: Record<string, string> }
): Promise<Stripe.Customer> {
return await this.stripe.customers.update(customerId, updates);
}
async deleteCustomer(customerId: string): Promise<Stripe.Customer> {
return await this.stripe.customers.del(customerId);
}
getDashboardUrl(): string {
return "https://dashboard.stripe.com";
}
getWebhookEndpointUrl(): string {
return `/api/webhooks/stripe`;
}
}
export const createStripeService = (config: StripeDashboardConfig): StripeService => {
return new StripeService(config);
};