From ce94a5d856edf79acabc9299f624fba5cdc56e62 Mon Sep 17 00:00:00 2001 From: arunasrivastava Date: Tue, 23 Jun 2026 20:29:56 -0700 Subject: [PATCH 1/2] Add beta redirect click tracking --- app/beta/[platform]/route.ts | 49 ++++++++ app/blog/[slug]/page.tsx | 16 +-- components/ui/base/dialog.tsx | 12 +- db/schema.ts | 16 ++- middleware.ts | 1 + migrations/0002_beta_redirect_clicks.sql | 13 ++ migrations/meta/0002_snapshot.json | 148 +++++++++++++++++++++++ migrations/meta/_journal.json | 9 +- 8 files changed, 246 insertions(+), 18 deletions(-) create mode 100644 app/beta/[platform]/route.ts create mode 100644 migrations/0002_beta_redirect_clicks.sql create mode 100644 migrations/meta/0002_snapshot.json diff --git a/app/beta/[platform]/route.ts b/app/beta/[platform]/route.ts new file mode 100644 index 0000000..7692f4b --- /dev/null +++ b/app/beta/[platform]/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { betaRedirectClicks, db } from '@/db/schema'; + +const destinations: Record> = { + android: { + testing: 'https://play.google.com/apps/testing/com.slayspeech.app', + store: 'https://play.google.com/store/apps/details?id=com.slayspeech.app', + }, +}; + +const cleanParam = (value: string | null, maxLength: number) => { + const trimmed = value?.trim(); + if (!trimmed) return null; + return trimmed.slice(0, maxLength); +}; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ platform: string }> }, +) { + const { platform: rawPlatform } = await params; + const platform = rawPlatform.toLowerCase(); + const searchParams = request.nextUrl.searchParams; + const destination = cleanParam(searchParams.get('destination'), 64) || 'testing'; + const target = destinations[platform]?.[destination]; + + if (!target) { + return NextResponse.json({ error: 'Unsupported beta redirect target' }, { status: 404 }); + } + + try { + await db.insert(betaRedirectClicks).values({ + platform, + destination, + campaign: cleanParam(searchParams.get('campaign') || searchParams.get('utm_campaign'), 128), + variant: cleanParam(searchParams.get('variant') || searchParams.get('utm_content'), 128), + source: cleanParam(searchParams.get('source') || searchParams.get('utm_source'), 128), + medium: cleanParam(searchParams.get('medium') || searchParams.get('utm_medium'), 128), + recipientId: cleanParam(searchParams.get('recipient_id'), 128), + referrer: cleanParam(request.headers.get('referer'), 2048), + userAgent: cleanParam(request.headers.get('user-agent'), 2048), + }); + } catch (error) { + console.error('Failed to record beta redirect click', error); + } + + return NextResponse.redirect(target, { status: 302 }); +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index fe4e603..2944a49 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -7,8 +7,9 @@ import Header from '@/components/ui/1 - header'; import CTA from '@/components/sections/3 - CTA'; import Footer from '@/components/sections/4 - Footer'; -export async function generateMetadata({ params }: { params: { slug: string } }) { - const post = await getPost(params.slug); +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const post = await getPost(slug); if (!post) return {}; return { @@ -38,14 +39,9 @@ export async function generateMetadata({ params }: { params: { slug: string } }) }; } -export default async function PostPage({ - params, -}: { - params: { - slug: string; - }; -}) { - const post = await getPost(params.slug); +export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const post = await getPost(slug); if (!post) return notFound(); return ( diff --git a/components/ui/base/dialog.tsx b/components/ui/base/dialog.tsx index 328d3c5..00053d8 100644 --- a/components/ui/base/dialog.tsx +++ b/components/ui/base/dialog.tsx @@ -22,7 +22,7 @@ function DialogClose({ ...props }) { return ; } -function DialogOverlay({ className, ...props }) { +function DialogOverlay({ className = '', ...props }) { return ( @@ -57,7 +57,7 @@ function DialogContent({ className, children, ...props }) { ); } -function DialogHeader({ className, ...props }) { +function DialogHeader({ className = '', ...props }) { return (
new Date()), }); + +export const betaRedirectClicks = pgTable('beta_redirect_clicks', { + id: serial('id').primaryKey(), + platform: varchar('platform', { length: 32 }).notNull(), + destination: varchar('destination', { length: 64 }).notNull(), + campaign: varchar('campaign', { length: 128 }), + variant: varchar('variant', { length: 128 }), + source: varchar('source', { length: 128 }), + medium: varchar('medium', { length: 128 }), + recipientId: varchar('recipient_id', { length: 128 }), + referrer: text('referrer'), + userAgent: text('user_agent'), + createdAt: timestamp('created_at', { mode: 'date', precision: 3 }).notNull().defaultNow(), +}); diff --git a/middleware.ts b/middleware.ts index f497cad..3be2118 100644 --- a/middleware.ts +++ b/middleware.ts @@ -8,6 +8,7 @@ const PUBLIC_URLS = [ new RegExp('^/pricing$'), new RegExp('^/about$'), new RegExp('^/contact$'), + new RegExp('^/beta/.*$'), ]; export async function middleware(request: NextRequest) { diff --git a/migrations/0002_beta_redirect_clicks.sql b/migrations/0002_beta_redirect_clicks.sql new file mode 100644 index 0000000..b69575d --- /dev/null +++ b/migrations/0002_beta_redirect_clicks.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS "beta_redirect_clicks" ( + "id" serial PRIMARY KEY NOT NULL, + "platform" varchar(32) NOT NULL, + "destination" varchar(64) NOT NULL, + "campaign" varchar(128), + "variant" varchar(128), + "source" varchar(128), + "medium" varchar(128), + "recipient_id" varchar(128), + "referrer" text, + "user_agent" text, + "created_at" timestamp (3) DEFAULT now() NOT NULL +); diff --git a/migrations/meta/0002_snapshot.json b/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..00c023e --- /dev/null +++ b/migrations/meta/0002_snapshot.json @@ -0,0 +1,148 @@ +{ + "id": "5fc88f2f-e84e-4c46-918b-c2ed3c08cd21", + "prevId": "ea981418-b0c7-40b4-ad88-4be6ea95338c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.beta_redirect_clicks": { + "name": "beta_redirect_clicks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "destination": { + "name": "destination", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "campaign": { + "name": "campaign", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "medium": { + "name": "medium", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "recipient_id": { + "name": "recipient_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "streak": { + "name": "streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stripe_id": { + "name": "stripe_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 85f148b..142483a 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1728112130005, "tag": "0001_zippy_snowbird", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1782260000000, + "tag": "0002_beta_redirect_clicks", + "breakpoints": true } ] -} \ No newline at end of file +} From 49ad6f5f84d6f619e1be11858e8716a3b785f264 Mon Sep 17 00:00:00 2001 From: arunasrivastava Date: Tue, 23 Jun 2026 20:33:49 -0700 Subject: [PATCH 2/2] Pin workflow checkout actions --- .../workflows/azure-static-web-apps-salmon-sky-0a6b6c10f.yml | 4 ++-- .github/workflows/gitleaks.yml | 2 +- .github/workflows/zizmor.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/azure-static-web-apps-salmon-sky-0a6b6c10f.yml b/.github/workflows/azure-static-web-apps-salmon-sky-0a6b6c10f.yml index 422a4e4..8e20918 100644 --- a/.github/workflows/azure-static-web-apps-salmon-sky-0a6b6c10f.yml +++ b/.github/workflows/azure-static-web-apps-salmon-sky-0a6b6c10f.yml @@ -10,7 +10,7 @@ on: - main permissions: - contents: read # Access to repository content for build and deploy + contents: read # Access to repository content for build and deploy jobs: build_and_deploy_job: @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest name: Build and Deploy Job steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 with: submodules: true lfs: false diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index b8330bb..2104820 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -10,7 +10,7 @@ jobs: name: gitleaks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 4928d7e..ddf91a3 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -10,7 +10,7 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: persist-credentials: false