Skip to content

Commit 6395292

Browse files
committed
feat(api): track api calls for analytics
1 parent 3bdc968 commit 6395292

File tree

7 files changed

+183
-1172
lines changed

7 files changed

+183
-1172
lines changed

apps/frontend/components/dataprovider/auth-router.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {
55
CommonDataProviderRegistration,
66
DataProviderView,
77
} from "./provider-view";
8-
import { spawn } from "@opensource-observer/utils";
98
import { RegistrationProps } from "../../lib/types/plasmic";
109
import { logger } from "../../lib/logger";
11-
import { analytics } from "../../lib/clients/segment";
10+
import { clientAnalytics } from "../../lib/clients/segment";
1211
import { supabaseClient } from "../../lib/clients/supabase";
12+
import { spawn } from "@opensource-observer/utils";
1313

1414
const DEFAULT_PLASMIC_VARIABLE = "auth";
1515

@@ -70,9 +70,12 @@ function AuthRouter(props: AuthRouterProps) {
7070
// Identify the user via Segment
7171
if (user) {
7272
spawn(
73-
analytics.identify(user.id, {
74-
name: user.user_metadata?.name,
75-
email: user.email,
73+
clientAnalytics!.identify({
74+
userId: user.id,
75+
traits: {
76+
name: user.user_metadata?.name,
77+
email: user.email,
78+
},
7679
}),
7780
);
7881
}

apps/frontend/components/widgets/analytics.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
import { useEffect } from "react";
44
import { usePathname, useSearchParams } from "next/navigation";
5+
import { clientAnalytics } from "../../lib/clients/segment";
56
import { spawn } from "@opensource-observer/utils";
6-
import { analytics } from "../../lib/clients/segment";
77

88
function Analytics() {
99
const pathname = usePathname();
1010
const searchParams = useSearchParams();
1111

1212
useEffect(() => {
13-
spawn(analytics.page());
13+
spawn(clientAnalytics!.page());
1414
}, [pathname, searchParams]);
1515

1616
return null;

apps/frontend/lib/auth/auth.ts

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import "server-only";
2+
3+
import { type NextRequest } from "next/server";
4+
import { supabasePrivileged } from "../clients/supabase";
5+
import { User as SupabaseUser } from "@supabase/supabase-js";
6+
import { serverAnalytics } from "../clients/segment";
7+
8+
type AnonUser = {
9+
role: "anonymous";
10+
};
11+
12+
type User = {
13+
role: "user";
14+
userId: string;
15+
email?: string;
16+
name: string;
17+
};
18+
19+
export type Session = AnonUser | User;
20+
21+
// HTTP headers
22+
const AUTH_PREFIX = "bearer";
23+
24+
// Supabase schema
25+
const API_KEY_TABLE = "api_keys";
26+
const USER_ID_COLUMN = "user_id";
27+
const API_KEY_COLUMN = "api_key";
28+
const DELETED_COLUMN = "deleted_at";
29+
const ALL_COLUMNS = `${USER_ID_COLUMN},${API_KEY_COLUMN},${DELETED_COLUMN}`;
30+
31+
const makeAnonUser = (): AnonUser => ({
32+
role: "anonymous",
33+
});
34+
const makeUser = (user: SupabaseUser): User => ({
35+
role: "user",
36+
userId: user.id,
37+
email: user.email,
38+
name: user.user_metadata.name,
39+
});
40+
41+
async function getSession(request: NextRequest): Promise<Session> {
42+
const headers = request.headers;
43+
const auth = headers.get("authorization");
44+
45+
// If no token provided, then return anonymous role
46+
if (!auth) {
47+
console.log(`auth: No token => anon`);
48+
return makeAnonUser();
49+
}
50+
51+
// Get the token
52+
const trimmedAuth = auth.trim();
53+
const token = trimmedAuth.toLowerCase().startsWith(AUTH_PREFIX)
54+
? trimmedAuth.slice(AUTH_PREFIX.length).trim()
55+
: trimmedAuth;
56+
57+
// Get the user by API token
58+
const { data: keyData, error: keyError } = await supabasePrivileged
59+
.from(API_KEY_TABLE)
60+
.select(ALL_COLUMNS)
61+
.eq(API_KEY_COLUMN, token);
62+
63+
if (keyError || !keyData) {
64+
console.warn(`auth: Error retrieving API keys => anon`, keyError);
65+
return makeAnonUser();
66+
}
67+
68+
// Filter out inactive/deleted keys
69+
const activeKeys = keyData.filter((x) => !x.deleted_at);
70+
if (activeKeys.length < 1) {
71+
console.log(`auth: API key not valid => anon`);
72+
return makeAnonUser();
73+
}
74+
75+
const userId = activeKeys[0].user_id;
76+
const { data: userData, error: userError } =
77+
await supabasePrivileged.auth.admin.getUserById(userId);
78+
if (userError || !userData) {
79+
console.warn(`auth: Error retrieving user data => anon`, userError);
80+
return makeAnonUser();
81+
}
82+
83+
console.log(`/api/auth: API key and user valid valid => user`);
84+
return makeUser(userData.user);
85+
}
86+
87+
export async function verifySession(request: NextRequest) {
88+
const session = await getSession(request);
89+
90+
const trackParams = {
91+
event: "api_call",
92+
properties: {
93+
path: request.nextUrl.pathname,
94+
},
95+
};
96+
97+
if (session.role === "user") {
98+
serverAnalytics!.track({
99+
userId: session.userId,
100+
...trackParams,
101+
});
102+
} else {
103+
serverAnalytics!.track({
104+
anonymousId: crypto.randomUUID(),
105+
...trackParams,
106+
});
107+
}
108+
109+
return session;
110+
}

apps/frontend/lib/clients/segment.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
import { Analytics } from "@segment/analytics-node";
12
import { AnalyticsBrowser } from "@segment/analytics-next";
23
import { SEGMENT_KEY } from "../config";
34

4-
export const analytics = AnalyticsBrowser.load({ writeKey: SEGMENT_KEY });
5+
export const serverAnalytics =
6+
typeof window === "undefined"
7+
? new Analytics({ writeKey: SEGMENT_KEY })
8+
: undefined;
9+
export const clientAnalytics =
10+
typeof window !== "undefined"
11+
? AnalyticsBrowser.load({ writeKey: SEGMENT_KEY })
12+
: undefined;

apps/frontend/middleware.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NextResponse } from "next/server";
2+
import type { NextRequest } from "next/server";
3+
import { verifySession } from "./lib/auth/auth";
4+
5+
export default async function middleware(request: NextRequest) {
6+
await verifySession(request);
7+
8+
return NextResponse.next();
9+
}
10+
11+
export const config = {
12+
matcher: "/api/:path*",
13+
};

apps/frontend/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"@mui/x-date-pickers": "^6.20.2",
4040
"@opensource-observer/utils": "workspace:*",
4141
"@plasmicapp/loader-nextjs": "^1.0.398",
42-
"@segment/analytics-next": "^1.70.0",
42+
"@segment/analytics-next": "^1.77.0",
43+
"@segment/analytics-node": "^2.2.1",
4344
"@supabase/auth-helpers-nextjs": "^0.8.7",
4445
"@supabase/auth-ui-react": "^0.4.7",
4546
"@supabase/auth-ui-shared": "^0.1.8",

0 commit comments

Comments
 (0)