Skip to content

Commit

Permalink
[WEB] Track page views to Mixpanel
Browse files Browse the repository at this point in the history
  • Loading branch information
samtgarson committed Apr 9, 2024
1 parent 5e0b26a commit b5bdc5d
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 1 deletion.
10 changes: 10 additions & 0 deletions web/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
export { GET, POST } from 'src/auth'

// export async function GET(request: NextRequest) {
// console.log(request.nextUrl.pathname)
// const res = await authGet(request)
//
// if (!request.nextUrl.pathname.startsWith('/api/auth/callback/')) return res
//
// console.log(res)
// return res
// }
6 changes: 6 additions & 0 deletions web/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { appUrl } from '@books-about-food/shared/utils/app-url'
import { getEnv } from '@books-about-food/shared/utils/get-env'
import NextAuth, { NextAuthConfig } from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import { identify } from './lib/tracking/identify'

export const authOptions = {
providers: [
Expand Down Expand Up @@ -95,6 +96,11 @@ export const authOptions = {
signIn: '/auth/sign-in',
signOut: '/account',
error: '/auth/sign-in'
},
events: {
async signIn({ user }) {
await identify(user)
}
}
} satisfies NextAuthConfig

Expand Down
5 changes: 5 additions & 0 deletions web/src/lib/tracking/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type TrackableEvents = {
'Opened a modal': { Extra?: Record<string, unknown>; Modal: string }
'Signed In': { Provider?: string; 'First Time': boolean }
'Viewed a page': { Path: string; Ref?: string | null }
}
25 changes: 25 additions & 0 deletions web/src/lib/tracking/identify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { User } from 'next-auth'

import { token, trackingEnabled } from './utils'

export const identify = async ({ id, name, email, image }: User) => {
if (!trackingEnabled) return
if (!token) return

const body = {
$token: token,
$distinct_id: id,
$ip: 0,
$set: {
$name: name,
$email: email,
$avatar: image
}
}

await fetch(`https://api.mixpanel.com/engage#profile-set?ip=0`, {
method: 'POST',
body: JSON.stringify([body]),
headers: { 'content-type': 'application/json', accept: 'text/plain' }
})
}
81 changes: 81 additions & 0 deletions web/src/lib/tracking/track.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use server'

import { cookies, headers } from 'next/headers'
import { NextRequest, NextResponse, userAgent } from 'next/server'

import { ResponseCookies } from 'next/dist/compiled/@edge-runtime/cookies'
import { TrackableEvents } from './events'
import { token, trackingEnabled } from './utils'

type CommonEventProperties = {
$browser?: string
$device?: string
$os?: string
$referrer?: string | null
ip?: string | null
}

export async function track<T extends keyof TrackableEvents>(
userId: string | undefined,
event: T,
properties: TrackableEvents[T],
req?: NextRequest,
res?: NextResponse
) {
if (!trackingEnabled) return
if (!token) return

const distinct_id =
userId ||
req?.cookies.get('baf-session-id')?.value ||
generateAnonymousId(res?.cookies)

const body = {
event,
properties: {
...getCommonProperties(req),
...properties,
distinct_id,
token
}
}

await fetch(`https://api.mixpanel.com/track?ip=0`, {
method: 'POST',
body: JSON.stringify([body]),
headers: { 'content-type': 'application/json', accept: 'text/plain' }
})
}

function getCommonProperties(req?: NextRequest): CommonEventProperties {
try {
const h = req?.headers || headers()
const ua = userAgent({ headers: h })

return {
$browser: ua.browser.name,
$device: ua.device.type,
$os: ua.os.name,
$referrer: h.get('referer'),
ip:
h.get('cf-connecting-ip') ||
req?.ip ||
h.get('x-real-ip') ||
h.get('x-forwarded-for')
}
} catch (e) {
return {}
}
}

function generateAnonymousId(c: ResponseCookies = cookies()) {
const anonId = crypto.randomUUID()
c.set({
name: 'baf-session-id',
value: anonId,
maxAge: 60 * 60 * 24 * 365,
secure: true,
httpOnly: true
})
return anonId
}
5 changes: 5 additions & 0 deletions web/src/lib/tracking/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const trackingEnabled =
process.env.NODE_ENV !== 'development' ||
process.env.ENABLE_TRACKING_IN_DEV === 'true'

export const token = process.env.MIXPANEL_TOKEN
18 changes: 17 additions & 1 deletion web/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { NextResponse } from 'next/server'
import { auth } from './auth'
import { track } from './lib/tracking/track'

const protectedPath = (pathname: string) =>
['/account', '/edit'].some((path) => pathname.startsWith(path))

const systemPath = (pathname: string) =>
['/api', '/_next', '/auth'].some((path) => pathname.startsWith(path))

export default auth(function middleware(request) {
export default auth(async function middleware(request) {
if (request.method === 'POST' || systemPath(request.nextUrl.pathname)) {
return NextResponse.next()
}
Expand All @@ -21,6 +22,21 @@ export default auth(function middleware(request) {
status: 307
})
}

const response = NextResponse.next()

await track(
user?.id,
'Viewed a page',
{
Path: request.nextUrl.pathname,
Ref: request.nextUrl.searchParams.get('ref')
},
request,
response
)

return response
})

export const config = {
Expand Down

0 comments on commit b5bdc5d

Please sign in to comment.