feat: implement analytics tracking for desktop app events

- Added event tracking for desktop app activation and quitting.
- Introduced analytics bridge in preload script to handle user identification and event capturing.
- Updated IPC channels to support analytics-related actions.
- Enhanced analytics functionality in the main process to track user interactions and application updates.
- Integrated analytics tracking for folder watching and deep link handling.
- Improved connector setup tracking in the web application.

This commit enhances the overall analytics capabilities of the application, ensuring better user behavior insights and event tracking across both desktop and web environments.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-18 14:35:14 -07:00
parent b38a297349
commit b440610e04
18 changed files with 673 additions and 80 deletions

View file

@ -1,7 +1,8 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { ReceiptText } from "lucide-react";
import { useQueries } from "@tanstack/react-query";
import { Coins, FileText, ReceiptText } from "lucide-react";
import { useMemo } from "react";
import { Badge } from "@/components/ui/badge";
import { Spinner } from "@/components/ui/spinner";
import {
@ -12,10 +13,26 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { PagePurchase, PagePurchaseStatus } from "@/contracts/types/stripe.types";
import type {
PagePurchase,
PagePurchaseStatus,
TokenPurchase,
} from "@/contracts/types/stripe.types";
import { stripeApiService } from "@/lib/apis/stripe-api.service";
import { cn } from "@/lib/utils";
type PurchaseKind = "pages" | "tokens";
type UnifiedPurchase = {
id: string;
kind: PurchaseKind;
created_at: string;
status: PagePurchaseStatus;
granted: number;
amount_total: number | null;
currency: string | null;
};
const STATUS_STYLES: Record<PagePurchaseStatus, { label: string; className: string }> = {
completed: {
label: "Completed",
@ -31,6 +48,22 @@ const STATUS_STYLES: Record<PagePurchaseStatus, { label: string; className: stri
},
};
const KIND_META: Record<
PurchaseKind,
{ label: string; icon: React.ComponentType<{ className?: string }>; iconClass: string }
> = {
pages: {
label: "Pages",
icon: FileText,
iconClass: "text-sky-500",
},
tokens: {
label: "Premium Tokens",
icon: Coins,
iconClass: "text-amber-500",
},
};
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString(undefined, {
year: "numeric",
@ -39,19 +72,65 @@ function formatDate(iso: string): string {
});
}
function formatAmount(purchase: PagePurchase): string {
if (purchase.amount_total == null) return "—";
const dollars = purchase.amount_total / 100;
const currency = (purchase.currency ?? "usd").toUpperCase();
return `$${dollars.toFixed(2)} ${currency}`;
function formatAmount(amount: number | null, currency: string | null): string {
if (amount == null) return "—";
const dollars = amount / 100;
const code = (currency ?? "usd").toUpperCase();
return `$${dollars.toFixed(2)} ${code}`;
}
function normalizePagePurchase(p: PagePurchase): UnifiedPurchase {
return {
id: p.id,
kind: "pages",
created_at: p.created_at,
status: p.status,
granted: p.pages_granted,
amount_total: p.amount_total,
currency: p.currency,
};
}
function normalizeTokenPurchase(p: TokenPurchase): UnifiedPurchase {
return {
id: p.id,
kind: "tokens",
created_at: p.created_at,
status: p.status,
granted: p.tokens_granted,
amount_total: p.amount_total,
currency: p.currency,
};
}
export function PurchaseHistoryContent() {
const { data, isLoading } = useQuery({
queryKey: ["stripe-purchases"],
queryFn: () => stripeApiService.getPurchases(),
const results = useQueries({
queries: [
{
queryKey: ["stripe-purchases"],
queryFn: () => stripeApiService.getPurchases(),
},
{
queryKey: ["stripe-token-purchases"],
queryFn: () => stripeApiService.getTokenPurchases(),
},
],
});
const [pagesQuery, tokensQuery] = results;
const isLoading = pagesQuery.isLoading || tokensQuery.isLoading;
const purchases = useMemo<UnifiedPurchase[]>(() => {
const pagePurchases = pagesQuery.data?.purchases ?? [];
const tokenPurchases = tokensQuery.data?.purchases ?? [];
return [
...pagePurchases.map(normalizePagePurchase),
...tokenPurchases.map(normalizeTokenPurchase),
].sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
}, [pagesQuery.data, tokensQuery.data]);
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
@ -60,15 +139,13 @@ export function PurchaseHistoryContent() {
);
}
const purchases = data?.purchases ?? [];
if (purchases.length === 0) {
return (
<div className="flex flex-col items-center justify-center gap-2 py-16 text-center">
<ReceiptText className="h-8 w-8 text-muted-foreground" />
<p className="text-sm font-medium">No purchases yet</p>
<p className="text-xs text-muted-foreground">
Your page-pack purchases will appear here after checkout.
Your page and premium token purchases will appear here after checkout.
</p>
</div>
);
@ -81,25 +158,36 @@ export function PurchaseHistoryContent() {
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead className="text-right">Pages</TableHead>
<TableHead>Type</TableHead>
<TableHead className="text-right">Granted</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead className="text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{purchases.map((p) => {
const style = STATUS_STYLES[p.status];
const statusStyle = STATUS_STYLES[p.status];
const kind = KIND_META[p.kind];
const KindIcon = kind.icon;
return (
<TableRow key={p.id}>
<TableRow key={`${p.kind}-${p.id}`}>
<TableCell className="text-sm">{formatDate(p.created_at)}</TableCell>
<TableCell className="text-right tabular-nums text-sm">
{p.pages_granted.toLocaleString()}
<TableCell className="text-sm">
<div className="flex items-center gap-2">
<KindIcon className={cn("h-4 w-4", kind.iconClass)} />
<span>{kind.label}</span>
</div>
</TableCell>
<TableCell className="text-right tabular-nums text-sm">
{formatAmount(p)}
{p.granted.toLocaleString()}
</TableCell>
<TableCell className="text-right tabular-nums text-sm">
{formatAmount(p.amount_total, p.currency)}
</TableCell>
<TableCell className="text-center">
<Badge className={cn("text-[10px]", style.className)}>{style.label}</Badge>
<Badge className={cn("text-[10px]", statusStyle.className)}>
{statusStyle.label}
</Badge>
</TableCell>
</TableRow>
);
@ -108,7 +196,8 @@ export function PurchaseHistoryContent() {
</Table>
</div>
<p className="text-center text-xs text-muted-foreground">
Showing your {purchases.length} most recent purchase{purchases.length !== 1 ? "s" : ""}.
Showing your {purchases.length} most recent purchase
{purchases.length !== 1 ? "s" : ""}.
</p>
</div>
);