revops + onboarding

This commit is contained in:
Timo Knuth
2026-04-22 20:00:44 +02:00
parent ce724662d4
commit 7d2724b65d
37 changed files with 5073 additions and 1286 deletions

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { hashIP } from '@/lib/hash';
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { hashIP } from '@/lib/hash';
import { triggerLifecycleScoring } from '@/lib/revops-server';
export async function GET(
request: NextRequest,
@@ -10,13 +11,14 @@ export async function GET(
const { slug } = await params;
// Fetch QR code by slug
const qrCode = await db.qRCode.findUnique({
where: { slug },
select: {
id: true,
content: true,
contentType: true,
},
const qrCode = await db.qRCode.findUnique({
where: { slug },
select: {
id: true,
userId: true,
content: true,
contentType: true,
},
});
if (!qrCode) {
@@ -24,7 +26,7 @@ export async function GET(
}
// Track scan (fire and forget)
trackScan(qrCode.id, request).catch(console.error);
trackScan(qrCode.id, qrCode.userId, request).catch(console.error);
// Determine destination URL
let destination = '';
@@ -121,7 +123,7 @@ export async function GET(
}
}
async function trackScan(qrId: string, request: NextRequest) {
async function trackScan(qrId: string, userId: string, request: NextRequest) {
try {
const userAgent = request.headers.get('user-agent') || '';
const referer = request.headers.get('referer') || '';
@@ -133,15 +135,30 @@ async function trackScan(qrId: string, request: NextRequest) {
const dnt = request.headers.get('dnt');
if (dnt === '1') {
// Respect Do Not Track - only increment counter
await db.qRScan.create({
data: {
qrId,
ipHash: 'dnt',
isUnique: false,
},
});
return;
}
const scanTimestamp = new Date();
await db.qRScan.create({
data: {
qrId,
ipHash: 'dnt',
isUnique: false,
ts: scanTimestamp,
},
});
const activatedUsers = await db.user.updateMany({
where: {
id: userId,
firstScanAt: null,
},
data: {
firstScanAt: scanTimestamp,
activationAt: scanTimestamp,
},
});
if (activatedUsers.count > 0) {
triggerLifecycleScoring(userId, 'scan_recorded');
}
return;
}
// Hash IP for privacy
const ipHash = hashIP(ip);
@@ -222,22 +239,39 @@ async function trackScan(qrId: string, request: NextRequest) {
const isUnique = !existingScan;
// Create scan record
await db.qRScan.create({
data: {
qrId,
ipHash,
userAgent: userAgent.substring(0, 255),
device,
const scanTimestamp = new Date();
await db.qRScan.create({
data: {
qrId,
ts: scanTimestamp,
ipHash,
userAgent: userAgent.substring(0, 255),
device,
os,
country,
referrer: referer.substring(0, 255),
utmSource,
utmMedium,
utmCampaign,
isUnique,
},
});
} catch (error) {
isUnique,
},
});
const activatedUsers = await db.user.updateMany({
where: {
id: userId,
firstScanAt: null,
},
data: {
firstScanAt: scanTimestamp,
activationAt: scanTimestamp,
},
});
if (activatedUsers.count > 0) {
triggerLifecycleScoring(userId, 'scan_recorded');
}
} catch (error) {
// Don't throw - this is fire and forget
}
}
@@ -250,4 +284,4 @@ function ensureAbsoluteUrl(url: string): string {
}
// Default to https for web URLs
return `https://${url}`;
}
}