chore(scope): enforce Fusion-ships-to-dev-only rule; remove prod-targeted e2e cruft
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Fusion is allowed to ship to dev.sase.tr (staging) only — the user owns the dev→main GitHub merge that promotes to production. Heartbeat-created tasks kept hallucinating prod deploys / monitoring (FN-291 / 315 / 323 / 324 / 311 etc.), and an entire e2e suite targeting live sase.tr was merged under FN-320 with hardcoded test creds. Changes: - Add "Project-wide rule: Fusion ships to dev.sase.tr only" block to all 7 agent personas (ceo/cto/cpo/designer/qa/backend-eng/frontend-eng). - Add /AGENTS.md at project root as the canonical rule (loaded once pi-markdown-workflows is reinstalled in the container). - Remove production-targeted e2e suite under apps/web/e2e/ (auth.setup.ts, deployment-audit.cjs, p0-subscription-cro.spec.ts, p0-subscription-cro-verify.cjs, playwright.config.ts, screenshots/). - Remove docs/product/funnel-monitoring-p0-cro-post-deploy-2026-05.md (prod-monitoring deliverable, out-of-scope). - Subscription downgrade-offer dialog: avoid mobile-viewport overflow by capping width at calc(100vw - 2rem) below sm breakpoint (formerly attempted in failed/duplicate FN-291/295).
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# Backend Eng — sase
|
||||
|
||||
## Project-wide rule: Fusion ships to `dev.sase.tr` only
|
||||
|
||||
Fusion's merge target is `dev`. Production (`sase.tr`/`main`) is **human-only** — the user manually merges `dev → main` on GitHub when ready. **Never** create or accept tasks like "deploy X to production", "production bundle is missing Y", "trigger PM2 restart", or verification scenarios targeting `sase.tr`. If a staged change is verified on `dev.sase.tr`, the task is done — production lag is expected and intentional. For prod-urgent escalations use `fn_send_message({to: "user"})`, not a new task.
|
||||
|
||||
## When consulted
|
||||
|
||||
- NestJS service/controller work: new module, endpoint, business logic.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# CEO — sase
|
||||
|
||||
## Project-wide rule: Fusion ships to `dev.sase.tr` only
|
||||
|
||||
Fusion's merge target is `dev`. Production (`sase.tr`/`main`) is **human-only** — the user manually merges `dev → main` on GitHub when ready. **Never** create or accept tasks like "deploy X to production", "production bundle is missing Y", "trigger PM2 restart", or verification scenarios targeting `sase.tr`. If a staged change is verified on `dev.sase.tr`, the task is done — production lag is expected and intentional. For prod-urgent escalations use `fn_send_message({to: "user"})`, not a new task.
|
||||
|
||||
## When consulted
|
||||
|
||||
- Strategic direction questions (what to build next, what to cut).
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# CPO — sase
|
||||
|
||||
## Project-wide rule: Fusion ships to `dev.sase.tr` only
|
||||
|
||||
Fusion's merge target is `dev`. Production (`sase.tr`/`main`) is **human-only** — the user manually merges `dev → main` on GitHub when ready. **Never** create or accept tasks like "deploy X to production", "production bundle is missing Y", "trigger PM2 restart", or verification scenarios targeting `sase.tr`. If a staged change is verified on `dev.sase.tr`, the task is done — production lag is expected and intentional. For prod-urgent escalations use `fn_send_message({to: "user"})`, not a new task.
|
||||
|
||||
## When consulted
|
||||
|
||||
- Product feature scoping (what to build, what to cut).
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# CTO — sase
|
||||
|
||||
## Project-wide rule: Fusion ships to `dev.sase.tr` only
|
||||
|
||||
Fusion's merge target is `dev`. Production (`sase.tr`/`main`) is **human-only** — the user manually merges `dev → main` on GitHub when ready. **Never** create or accept tasks like "deploy X to production", "production bundle is missing Y", "trigger PM2 restart", or verification scenarios targeting `sase.tr`. If a staged change is verified on `dev.sase.tr`, the task is done — production lag is expected and intentional. For prod-urgent escalations use `fn_send_message({to: "user"})`, not a new task.
|
||||
|
||||
## When consulted
|
||||
|
||||
- API/backend bug or feature scoping.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Designer — sase
|
||||
|
||||
## Project-wide rule: Fusion ships to `dev.sase.tr` only
|
||||
|
||||
Fusion's merge target is `dev`. Production (`sase.tr`/`main`) is **human-only** — the user manually merges `dev → main` on GitHub when ready. **Never** create or accept tasks like "deploy X to production", "production bundle is missing Y", "trigger PM2 restart", or verification scenarios targeting `sase.tr`. If a staged change is verified on `dev.sase.tr`, the task is done — production lag is expected and intentional. For prod-urgent escalations use `fn_send_message({to: "user"})`, not a new task.
|
||||
|
||||
## When consulted
|
||||
|
||||
- New UI / component spec (route, modal, form, schema viewer).
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Frontend Eng — sase
|
||||
|
||||
## Project-wide rule: Fusion ships to `dev.sase.tr` only
|
||||
|
||||
Fusion's merge target is `dev`. Production (`sase.tr`/`main`) is **human-only** — the user manually merges `dev → main` on GitHub when ready. **Never** create or accept tasks like "deploy X to production", "production bundle is missing Y", "trigger PM2 restart", or verification scenarios targeting `sase.tr`. If a staged change is verified on `dev.sase.tr`, the task is done — production lag is expected and intentional. For prod-urgent escalations use `fn_send_message({to: "user"})`, not a new task.
|
||||
|
||||
## When consulted
|
||||
|
||||
- New route or component (TanStack Router file-based).
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# QA Lead — sase
|
||||
|
||||
## Project-wide rule: Fusion ships to `dev.sase.tr` only
|
||||
|
||||
Fusion's merge target is `dev`. Production (`sase.tr`/`main`) is **human-only** — the user manually merges `dev → main` on GitHub when ready. **Never** create or accept tasks like "deploy X to production", "production bundle is missing Y", "trigger PM2 restart", or verification scenarios targeting `sase.tr`. If a staged change is verified on `dev.sase.tr`, the task is done — production lag is expected and intentional. For prod-urgent escalations use `fn_send_message({to: "user"})`, not a new task.
|
||||
|
||||
## When consulted
|
||||
|
||||
- Test strategy for a new feature (before/during BE/FE work).
|
||||
|
||||
46
AGENTS.md
Normal file
46
AGENTS.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Fusion Agent Operating Rules — sase.tr
|
||||
|
||||
This file is auto-loaded into every Fusion agent's context (via `pi-markdown-workflows`). It defines project-wide constraints that override per-persona instructions.
|
||||
|
||||
## Deployment scope (CRITICAL)
|
||||
|
||||
**Fusion only ships to `dev.sase.tr` (staging).** Production deploys to `sase.tr` (main branch) are **human-only**.
|
||||
|
||||
The pipeline:
|
||||
|
||||
```
|
||||
Fusion agent → merge to local `dev` → push to `github/dev` → Coolify → dev.sase.tr (staging)
|
||||
↓
|
||||
HUMAN reviews on dev.sase.tr
|
||||
↓
|
||||
HUMAN opens GitHub PR: dev → main
|
||||
↓
|
||||
GitHub Action → PM2 → sase.tr (prod)
|
||||
```
|
||||
|
||||
The user merges `dev → main` manually on GitHub when they decide the staging build is ready. Fusion has no role in that step.
|
||||
|
||||
### Do NOT create tasks that:
|
||||
|
||||
- "Deploy X to production" / "Push X to sase.tr" / "Trigger PM2 restart"
|
||||
- "Production bundle is missing key Y" / "Live site does not have feature Z" — production lag is **expected and intentional**; it reflects the unmerged dev→main gap, not a bug.
|
||||
- "Run CI/CD pipeline to ship the FN-NNN bundle to prod"
|
||||
- Any task whose acceptance criterion is "verified on sase.tr" (use dev.sase.tr instead)
|
||||
|
||||
If verification on staging passes, **the task is done.** Do not chase whether the change reached production — that's the user's call.
|
||||
|
||||
### Acceptable verification targets
|
||||
|
||||
- `dev.sase.tr` — staging, where Fusion's work lands
|
||||
- Local worktree + `pnpm test` — for unit/integration coverage
|
||||
- Local `pnpm dev` — for visual/manual checks
|
||||
|
||||
### Acceptable escalation
|
||||
|
||||
If you genuinely believe a staged change needs to reach production urgently, use `fn_send_message({to: "user"})` with a one-line summary. Do NOT create a Fusion task for it.
|
||||
|
||||
## Branch policy
|
||||
|
||||
- Fusion always works against `dev`. Worktrees branch from `dev`. Merges target `dev`.
|
||||
- Never push to `github/main` or `origin/main`.
|
||||
- Never create a PR from `dev` to `main` — that is the user's manual step.
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Auth setup — logs in with admin credentials and saves storage state.
|
||||
*/
|
||||
import { test as setup, expect } from "@playwright/test";
|
||||
|
||||
const ADMIN_EMAIL = "admin@sase.tr";
|
||||
const ADMIN_PASSWORD = "Sase2026";
|
||||
|
||||
setup("authenticate as admin", async ({ page }) => {
|
||||
await page.goto("https://sase.tr/login");
|
||||
await expect(page.getByRole("heading", { name: /giriş/i })).toBeVisible();
|
||||
|
||||
await page.getByLabel(/e-posta|email/i).fill(ADMIN_EMAIL);
|
||||
await page.getByLabel(/şifre|password/i).fill(ADMIN_PASSWORD);
|
||||
await page.getByRole("button", { name: /giriş yap|sign in/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard\/?$/);
|
||||
await page.context().storageState({ path: ".auth/admin.json" });
|
||||
});
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* Production deployment verification: subscription i18n translation key audit.
|
||||
*
|
||||
* Compares tr.json subscription i18n keys against the keys found in the
|
||||
* deployed JavaScript bundle on sase.tr. Identifies P0 CRO fixes that have
|
||||
* source code but are missing from the deployed production bundle.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
|
||||
// Source keys from apps/web/src/messages/tr.json (subscription section)
|
||||
// These are the P0-fixed keys that should be present
|
||||
const sourceSubscriptionKeys = [
|
||||
"title", "noSubscription", "choosePlan", "subscribe", "currentPlan",
|
||||
"cancelSubscription", "cancelling", "cancelConfirmTitle",
|
||||
"cancelConfirmDescription", "cancelled", "resumeSubscription",
|
||||
"resuming", "resumed", "accessibleBrands", "endDate", "startDate",
|
||||
"status", "billingPeriod", "planComparison", "featureMatrix",
|
||||
"selectBrands", "selectBrandsDescription", "brandsSelected",
|
||||
"allBrandsSelected", "maxBrandsReached", "popular", "yearlyDiscount",
|
||||
"proceed", "orderSummary", "orderSummaryPlan", "orderSummaryPeriod",
|
||||
"orderSummaryBrands", "orderSummaryPrice", "trialTitle",
|
||||
"trialDescription", "startTrial", "trialStarted", "daysLeft",
|
||||
"trustNoCard", "trustCancelAnytime", "trustRefund",
|
||||
"paymentTrustSSL", "paymentTrustProvider", "paymentTrustKVKK",
|
||||
"trialDaysLeft",
|
||||
];
|
||||
|
||||
// Keys actually found in the deployed sase.tr production JS bundle
|
||||
const deployedSubscriptionKeys = [
|
||||
"title", "noSubscription", "choosePlan", "subscribe", "currentPlan",
|
||||
"cancelSubscription", "cancelling", "cancelConfirmTitle",
|
||||
"cancelConfirmDescription", "cancelled", "resumeSubscription",
|
||||
"resuming", "resumed", "accessibleBrands", "endDate", "startDate",
|
||||
"status", "billingPeriod", "planComparison",
|
||||
"selectBrands", "selectBrandsDescription", "brandsSelected",
|
||||
"allBrandsSelected", "maxBrandsReached", "popular",
|
||||
"trialTitle", "trialDescription", "startTrial", "trialStarted",
|
||||
];
|
||||
|
||||
const missingKeys = sourceSubscriptionKeys.filter(
|
||||
(k) => !deployedSubscriptionKeys.includes(k),
|
||||
);
|
||||
|
||||
const p0Mapping = {
|
||||
"P0-1 (Yearly discount badge)": ["yearlyDiscount"],
|
||||
"P0-3 (CTA text progression Plan Seç → Devam Et)": ["proceed"],
|
||||
"P0-4 (Order summary card)": [
|
||||
"orderSummary", "orderSummaryPlan", "orderSummaryPeriod",
|
||||
"orderSummaryBrands", "orderSummaryPrice",
|
||||
],
|
||||
"P0-7 (Trust copy: SSL, iyzico, KVKK)": [
|
||||
"paymentTrustSSL", "paymentTrustProvider", "paymentTrustKVKK",
|
||||
],
|
||||
"P0-9 (No hardcoded strings / i18n coverage)": [
|
||||
"featureMatrix", "daysLeft", "trialDaysLeft",
|
||||
"trustNoCard", "trustCancelAnytime", "trustRefund",
|
||||
],
|
||||
};
|
||||
|
||||
console.log("═".repeat(60));
|
||||
console.log("PRODUCTION DEPLOYMENT i18n AUDIT");
|
||||
console.log("Target: https://sase.tr");
|
||||
console.log("Bundle: index-BKd5Du15.js (5,048,294 bytes)");
|
||||
console.log("═".repeat(60));
|
||||
console.log();
|
||||
|
||||
console.log(`Source tr.json subscription keys: ${sourceSubscriptionKeys.length}`);
|
||||
console.log(`Deployed bundle subscription keys: ${deployedSubscriptionKeys.length}`);
|
||||
console.log(`MISSING keys: ${missingKeys.length}`);
|
||||
console.log();
|
||||
|
||||
console.log("P0 Impact Analysis:");
|
||||
console.log("─".repeat(40));
|
||||
for (const [scenario, keys] of Object.entries(p0Mapping)) {
|
||||
const missing = keys.filter((k) => missingKeys.includes(k));
|
||||
if (missing.length > 0) {
|
||||
console.log(`❌ ${scenario}`);
|
||||
for (const k of missing) {
|
||||
console.log(` Missing key: "${k}"`);
|
||||
}
|
||||
console.log(` Impact: t() fallback will show raw key name instead of Turkish text`);
|
||||
} else {
|
||||
console.log(`✅ ${scenario}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log("All MISSING keys:");
|
||||
console.log("─".repeat(40));
|
||||
for (const k of missingKeys) {
|
||||
console.log(` ❌ ${k}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// P0 scenarios that are verified as PRESENT in the deployed bundle
|
||||
console.log("P0 scenarios VERIFIED as PRESENT in deployed bundle:");
|
||||
console.log("─".repeat(40));
|
||||
console.log(" ✅ P0-2: Popular plan styling (ring-2, shadow-lg in CSS)");
|
||||
console.log(" ✅ P0-5: Current plan badge (currentPlan key, green styling)");
|
||||
console.log(" ✅ P0-6: Trial CTA visibility control (eligibleForTrial logic)");
|
||||
console.log(" ✅ P0-8: Skeleton loading states (animate-pulse, Skeleton component)");
|
||||
console.log(" ✅ P0-10: PostHog event capture (plan_selected, checkout_started, etc.)");
|
||||
console.log();
|
||||
|
||||
// Generate JSON report
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
target: "https://sase.tr",
|
||||
bundleHash: "index-BKd5Du15.js",
|
||||
summary: {
|
||||
sourceKeys: sourceSubscriptionKeys.length,
|
||||
deployedKeys: deployedSubscriptionKeys.length,
|
||||
missingKeys: missingKeys.length,
|
||||
},
|
||||
p0Impact: Object.entries(p0Mapping).map(([scenario, keys]) => ({
|
||||
scenario,
|
||||
keys,
|
||||
missing: keys.filter((k) => missingKeys.includes(k)),
|
||||
status: keys.filter((k) => missingKeys.includes(k)).length > 0 ? "FAIL" : "PASS",
|
||||
})),
|
||||
missingKeys,
|
||||
verifiedPresent: ["P0-2", "P0-5", "P0-6", "P0-8", "P0-10"],
|
||||
};
|
||||
|
||||
fs.mkdirSync("e2e/screenshots", { recursive: true });
|
||||
fs.writeFileSync(
|
||||
"e2e/screenshots/p0-deployment-audit.json",
|
||||
JSON.stringify(report, null, 2),
|
||||
);
|
||||
console.log("📝 Report saved to e2e/screenshots/p0-deployment-audit.json");
|
||||
|
||||
// Also save the full verification report combining bundle + HTML analysis
|
||||
const verificationReport = {
|
||||
timestamp: new Date().toISOString(),
|
||||
method: "Bundle-level verification (no browser automation available — sandbox lacks GUI libraries)",
|
||||
target: "https://sase.tr",
|
||||
testAccount: "admin@sase.tr",
|
||||
results: [
|
||||
{ id: "P0-1", name: "Yearly discount badge", passed: false, reason: "yearlyDiscount i18n key missing from deployed bundle" },
|
||||
{ id: "P0-2", name: "Popular plan visual distinction", passed: true, reason: "ring-2/shadow-brand/lg CSS classes present in deployed bundle" },
|
||||
{ id: "P0-3", name: "CTA button text progression", passed: false, reason: "proceed i18n key missing; deployed code uses 'subscribe' not 'proceed'" },
|
||||
{ id: "P0-4", name: "Order summary card", passed: false, reason: "orderSummary* i18n keys missing from deployed bundle" },
|
||||
{ id: "P0-5", name: "Current plan Mevcut Plan badge", passed: true, reason: "currentPlan i18n key present, green styling classes in CSS" },
|
||||
{ id: "P0-6", name: "Trial CTA hidden during active sub", passed: true, reason: "eligibleForTrial conditional logic present in deployed code" },
|
||||
{ id: "P0-7", name: "Trust copy (SSL, iyzico, KVKK)", passed: false, reason: "paymentTrust* i18n keys missing; SSL/KVKK from other contexts only" },
|
||||
{ id: "P0-8", name: "Skeleton loading states", passed: true, reason: "animate-pulse and Skeleton component present in deployed bundle" },
|
||||
{ id: "P0-9", name: "No hardcoded Turkish strings", passed: false, reason: "16 i18n keys missing — t() fallback shows raw key names" },
|
||||
{ id: "P0-10", name: "PostHog events firing", passed: true, reason: "PostHog capture events (plan_selected, checkout_started, etc.) present in bundle" },
|
||||
],
|
||||
summary: {
|
||||
total: 10,
|
||||
passed: 5,
|
||||
failed: 5,
|
||||
},
|
||||
criticalFinding: "The deployed sase.tr bundle (index-BKd5Du15.js) is missing 16 subscription i18n translation keys that exist in the source code (apps/web/src/messages/tr.json). This means the P0 CRO fixes exist in the repository source code but have NOT been deployed to production. A redeployment is required.",
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
"e2e/screenshots/p0-verification-report.json",
|
||||
JSON.stringify(verificationReport, null, 2),
|
||||
);
|
||||
console.log("📝 Full verification report saved to e2e/screenshots/p0-verification-report.json");
|
||||
|
||||
process.exit(missingKeys.length > 0 ? 1 : 0);
|
||||
@@ -1,324 +0,0 @@
|
||||
/**
|
||||
* P0-1 through P0-10 subscription page CRO verification script.
|
||||
*
|
||||
* Uses HTTP requests + JSDOM to verify the live sase.tr subscription page
|
||||
* against all P0 CRO fixes. Cannot use Playwright in this sandbox environment
|
||||
* (no GUI libraries available). Instead, verifies HTML structure and content.
|
||||
*
|
||||
* Run: node e2e/p0-subscription-cro-verify.cjs
|
||||
*/
|
||||
const { JSDOM } = require("jsdom");
|
||||
const fs = require("fs");
|
||||
|
||||
const BASE_URL = "https://sase.tr";
|
||||
const API_URL = `${BASE_URL}/api`;
|
||||
const ADMIN_EMAIL = "admin@sase.tr";
|
||||
const ADMIN_PASSWORD = "Sase2026";
|
||||
|
||||
const results = [];
|
||||
|
||||
function record(id, name, passed, details) {
|
||||
results.push({ id, name, passed, details });
|
||||
const status = passed ? "✅" : "❌";
|
||||
console.log(`${status} ${id}: ${name}`);
|
||||
if (details) console.log(` ${details}`);
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const res = await fetch(`${API_URL}/auth/sign-in/email`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
body: JSON.stringify({ email: ADMIN_EMAIL, password: ADMIN_PASSWORD }),
|
||||
redirect: "manual",
|
||||
});
|
||||
const rawCookies = res.headers.getSetCookie
|
||||
? res.headers.getSetCookie()
|
||||
: [res.headers.get("set-cookie") || ""];
|
||||
const cookies = rawCookies.join("; ");
|
||||
console.log(` Login status: ${res.status}`);
|
||||
if (res.status >= 400) {
|
||||
const body = await res.text();
|
||||
console.log(` Response: ${body.substring(0, 300)}`);
|
||||
throw new Error(`Login failed with status ${res.status}`);
|
||||
}
|
||||
console.log(` Cookies received: ${cookies.length} chars`);
|
||||
return cookies;
|
||||
}
|
||||
|
||||
async function fetchPage(path, cookies) {
|
||||
const url = `${BASE_URL}${path}`;
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Cookie: cookies,
|
||||
Origin: BASE_URL,
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
|
||||
},
|
||||
redirect: "follow",
|
||||
});
|
||||
const html = await res.text();
|
||||
console.log(` GET ${path} → ${res.status}, ${html.length} bytes`);
|
||||
return new JSDOM(html, { url });
|
||||
}
|
||||
|
||||
function q(doc, selector) {
|
||||
return doc.querySelector(selector);
|
||||
}
|
||||
|
||||
function qa(doc, selector) {
|
||||
return Array.from(doc.querySelectorAll(selector));
|
||||
}
|
||||
|
||||
function hasText(doc, texts) {
|
||||
const bodyText = (doc.body && doc.body.textContent) || "";
|
||||
const lowerBody = bodyText.toLowerCase();
|
||||
return texts.some((t) => lowerBody.includes(t.toLowerCase()));
|
||||
}
|
||||
|
||||
// ─── MAIN ───────────────────────────────────────────────────────────────────
|
||||
async function main() {
|
||||
console.log("🔐 Logging in as admin...");
|
||||
const cookies = await login();
|
||||
|
||||
// --- Fetch subscription page ---
|
||||
console.log("\n📄 Fetching subscription page...");
|
||||
const subDom = await fetchPage("/dashboard/subscription", cookies);
|
||||
const subDoc = subDom.window.document;
|
||||
|
||||
// --- Fetch payment page ---
|
||||
console.log("\n📄 Fetching payment page...");
|
||||
const payDom = await fetchPage(
|
||||
"/dashboard/subscription/pay?plan=brand2&period=monthly&brands=",
|
||||
cookies,
|
||||
);
|
||||
const payDoc = payDom.window.document;
|
||||
|
||||
// Save HTML snapshots for documentation
|
||||
fs.mkdirSync("e2e/screenshots", { recursive: true });
|
||||
fs.writeFileSync(
|
||||
"e2e/screenshots/p0-subscription-page.html",
|
||||
subDom.serialize(),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
"e2e/screenshots/p0-payment-page.html",
|
||||
payDom.serialize(),
|
||||
);
|
||||
console.log(" HTML snapshots saved.");
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-1: Yearly discount badge visible on toggle
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-1: Yearly discount badge ──");
|
||||
const yearlyDiscountTexts = ["indirim", "discount", "tasarruf", "save"];
|
||||
const p01 = hasText(subDoc, yearlyDiscountTexts);
|
||||
record("P0-1", "Yearly discount badge visible", p01, "");
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-2: Popular plan (brand2) visually distinct
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-2: Popular plan visually distinct ──");
|
||||
const popularTexts = ["Popüler", "Popular"];
|
||||
const p02badge = hasText(subDoc, popularTexts);
|
||||
const popularEls = qa(subDoc, '[class*="popular"], [class*="Popüler"]');
|
||||
const ringEls = qa(subDoc, '[class*="ring-2"]');
|
||||
const shadowEls = qa(subDoc, '[class*="shadow-brand"]');
|
||||
const p02visual = p02badge && (popularEls.length > 0 || ringEls.length > 0);
|
||||
record(
|
||||
"P0-2",
|
||||
"Popular plan visually distinct",
|
||||
p02visual,
|
||||
`Badge=${p02badge} PopularEls=${popularEls.length} RingEls=${ringEls.length} ShadowEls=${shadowEls.length}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-3: CTA button text progression (Plan Seç → Devam Et)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-3: CTA button text progression ──");
|
||||
const hasPlanSec = hasText(subDoc, ["Plan Seç", "Choose Plan"]);
|
||||
const hasDevamEt = hasText(subDoc, ["Devam Et", "Proceed"]);
|
||||
record(
|
||||
"P0-3",
|
||||
"CTA button text progression",
|
||||
hasPlanSec || hasDevamEt,
|
||||
`PlanSeç=${hasPlanSec} DevamEt=${hasDevamEt}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-4: Order summary card visible before payment
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-4: Order summary card ──");
|
||||
const p04text = hasText(subDoc, ["Sipariş Özeti", "Order Summary"]);
|
||||
const p04pay = hasText(payDoc, [
|
||||
"Sipariş Özeti",
|
||||
"Order Summary",
|
||||
"Toplam Tutar",
|
||||
]);
|
||||
record(
|
||||
"P0-4",
|
||||
"Order summary card visible",
|
||||
p04text || p04pay,
|
||||
`On subscription: ${p04text}, On payment: ${p04pay}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-5: Current plan shows non-interactive Mevcut Plan badge
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-5: Current plan Mevcut Plan badge ──");
|
||||
const p05mevcut = hasText(subDoc, ["Mevcut Plan", "Current Plan"]);
|
||||
const greenEls = qa(subDoc, '[class*="green"]');
|
||||
const greenWithMevcut = greenEls.some((el) =>
|
||||
(el.textContent || "").toLowerCase().includes("mevcut"),
|
||||
);
|
||||
record(
|
||||
"P0-5",
|
||||
"Current plan shows Mevcut Plan badge",
|
||||
p05mevcut,
|
||||
`Mevcut text: ${p05mevcut}, Green badge: ${greenWithMevcut}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-6: Trial CTA hidden during active subscription
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-6: Trial CTA hidden ──");
|
||||
const p06trial = hasText(subDoc, [
|
||||
"Ücretsiz Denemeyi Başlat",
|
||||
"Start Free Trial",
|
||||
"Start Trial",
|
||||
]);
|
||||
const p06active = hasText(subDoc, ["Aktif", "Active"]);
|
||||
const p06Passed = !p06trial && p06active;
|
||||
record(
|
||||
"P0-6",
|
||||
"Trial CTA hidden during active subscription",
|
||||
p06Passed,
|
||||
`Trial CTA shown: ${p06trial}, Active status: ${p06active}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-7: Trust copy present on payment page (SSL, provider, KVKK)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-7: Trust copy ──");
|
||||
const trustSection = q(
|
||||
subDoc,
|
||||
'[aria-label*="güvencesi"], [aria-label*="trust"]',
|
||||
);
|
||||
const p07ssl = hasText(subDoc, ["SSL", "256-bit"]);
|
||||
const p07provider = hasText(subDoc, ["iyzico", "Iyzico"]);
|
||||
const p07kvkk = hasText(subDoc, ["KVKK"]);
|
||||
const p07payssl = hasText(payDoc, ["SSL", "3D Secure", "güvenli"]);
|
||||
const p07payprovider = hasText(payDoc, ["iyzico"]);
|
||||
const p07Passed =
|
||||
(p07ssl || p07provider || p07kvkk) && (p07payssl || p07payprovider);
|
||||
record(
|
||||
"P0-7",
|
||||
"Trust copy present (SSL, iyzico, KVKK)",
|
||||
p07Passed,
|
||||
`SubPage: SSL=${p07ssl} Provider=${p07provider} KVKK=${p07kvkk} | PayPage: SSL=${p07payssl} Provider=${p07payprovider} TrustSection=${!!trustSection}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-8: Skeleton loading states correct
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-8: Skeleton loading states ──");
|
||||
const skeletonEls = qa(subDoc, ".animate-pulse");
|
||||
const paySkeletonEls = qa(payDoc, ".animate-pulse");
|
||||
const hasHeading = q(subDoc, "h2, h3, [class*='font-bold']");
|
||||
record(
|
||||
"P0-8",
|
||||
"Skeleton loading states correct",
|
||||
!!hasHeading,
|
||||
`Skeleton elements: ${skeletonEls.length}, Pay skeletons: ${paySkeletonEls.length}, Has heading: ${!!hasHeading}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-9: No hardcoded Turkish strings
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-9: No hardcoded Turkish strings ──");
|
||||
const expectedI18nKeys = [
|
||||
"Abonelik",
|
||||
"Plan Seç",
|
||||
"Plan Karşılaştırması",
|
||||
"Özellik Karşılaştırması",
|
||||
];
|
||||
const bodyText = subDoc.body ? subDoc.body.textContent || "" : "";
|
||||
const allFound = expectedI18nKeys.every((key) => bodyText.includes(key));
|
||||
const hasEnglish = bodyText.includes("Subscription");
|
||||
record(
|
||||
"P0-9",
|
||||
"No hardcoded Turkish strings (i18n working)",
|
||||
allFound || hasEnglish,
|
||||
`TR keys found: ${expectedI18nKeys.map((k) => bodyText.includes(k)).join(",")}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// P0-10: PostHog events firing
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n── P0-10: PostHog events ──");
|
||||
const scripts = qa(subDoc, "script");
|
||||
const hasPosthogScript = scripts.some(
|
||||
(s) =>
|
||||
(s.getAttribute("src") || "").includes("posthog") ||
|
||||
(s.textContent || "").includes("posthog"),
|
||||
);
|
||||
const hasPosthogInit = bodyText.includes("posthog");
|
||||
const hasFaro = scripts.some(
|
||||
(s) =>
|
||||
(s.getAttribute("src") || "").includes("faro") ||
|
||||
(s.textContent || "").includes("faro"),
|
||||
);
|
||||
record(
|
||||
"P0-10",
|
||||
"PostHog events firing (script loaded)",
|
||||
hasPosthogScript || hasPosthogInit,
|
||||
`PostHog script: ${hasPosthogScript} PostHog init: ${hasPosthogInit} Faro: ${hasFaro}`,
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SUMMARY
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log("\n" + "═".repeat(60));
|
||||
console.log("VERIFICATION SUMMARY");
|
||||
console.log("═".repeat(60));
|
||||
const passed = results.filter((r) => r.passed).length;
|
||||
const failed = results.filter((r) => !r.passed).length;
|
||||
console.log(
|
||||
`Total: ${results.length} | ✅ ${passed} passed | ❌ ${failed} failed`,
|
||||
);
|
||||
console.log("─".repeat(60));
|
||||
for (const r of results) {
|
||||
const status = r.passed ? "✅" : "❌";
|
||||
console.log(` ${status} ${r.id}: ${r.name}`);
|
||||
}
|
||||
console.log("═".repeat(60));
|
||||
|
||||
// Save results to file
|
||||
const reportDir = "e2e/screenshots";
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
baseUrl: BASE_URL,
|
||||
account: ADMIN_EMAIL,
|
||||
results,
|
||||
summary: { total: results.length, passed, failed },
|
||||
};
|
||||
fs.writeFileSync(
|
||||
`${reportDir}/p0-verification-report.json`,
|
||||
JSON.stringify(report, null, 2),
|
||||
);
|
||||
console.log(`\n📝 Report saved to ${reportDir}/p0-verification-report.json`);
|
||||
|
||||
// Exit with error if any tests failed
|
||||
if (failed > 0) {
|
||||
console.log("\n❌ Some verification checks failed!");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\n✅ All verification checks passed!");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("\n❌ Fatal error:", err.message);
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,353 +0,0 @@
|
||||
/**
|
||||
* P0-1 through P0-10 subscription page CRO verification tests.
|
||||
*
|
||||
* Tests run against live https://sase.tr with admin account (admin@sase.tr / Sase2026).
|
||||
* The admin has an active subscription — tests verify the subscription page renders
|
||||
* correctly for an active subscriber.
|
||||
*
|
||||
* P0 scenarios:
|
||||
* P0-1: Yearly discount badge visible on toggle
|
||||
* P0-2: Popular plan (brand2) visually distinct (shadow, ring, scale)
|
||||
* P0-3: CTA button text progression (Plan Seç → Devam Et)
|
||||
* P0-4: Order summary card visible before payment
|
||||
* P0-5: Current plan shows non-interactive Mevcut Plan badge
|
||||
* P0-6: Trial CTA hidden during trial (active subscriber → no trial CTA)
|
||||
* P0-7: Trust copy present on payment page (SSL, provider, KVKK)
|
||||
* P0-8: Skeleton loading states correct
|
||||
* P0-9: No hardcoded Turkish strings (all text from i18n)
|
||||
* P0-10: PostHog events firing (network tab)
|
||||
*/
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const SCREENSHOT_DIR = "e2e/screenshots";
|
||||
|
||||
test.describe("P0 Subscription Page CRO Verification", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to subscription page — storage state auth means we're already logged in
|
||||
await page.goto("/dashboard/subscription", { waitUntil: "networkidle" });
|
||||
// Wait for the page to fully load (skeleton → content)
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /abonelik|subscription/i }),
|
||||
).toBeVisible({ timeout: 30000 });
|
||||
});
|
||||
|
||||
// ── P0-1: Yearly discount badge ─────────────────────────────────────────
|
||||
test("P0-1: Yearly discount badge visible on toggle", async ({ page }) => {
|
||||
// Click "Yıllık" toggle
|
||||
const yearlyBtn = page.getByRole("button", { name: /yıllık|yearly/i });
|
||||
await yearlyBtn.click();
|
||||
|
||||
// Wait for plan cards to re-render with yearly prices
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Each plan card should show the yearly discount badge when in yearly mode
|
||||
// The badge text is "17% indirim" (Turkish) or similar
|
||||
const discountBadges = page.getByText(/indirim|discount/i, { exact: false });
|
||||
const badgeCount = await discountBadges.count();
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-1-yearly-discount.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// At least one plan card should show the yearly discount badge
|
||||
// (the yearly discount badge renders client-side after billing period change)
|
||||
expect(badgeCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// ── P0-2: Popular plan (brand2) visually distinct ───────────────────────
|
||||
test("P0-2: Popular plan (brand2) visually distinct", async ({ page }) => {
|
||||
// The "Popüler" badge should be visible
|
||||
const popularBadge = page.getByText(/popüler|popular/i, { exact: false }).first();
|
||||
await expect(popularBadge).toBeVisible();
|
||||
|
||||
// The brand2 plan card (parent of the "Popüler" badge) should have
|
||||
// special styling: primary border, shadow, ring, background tint
|
||||
const popularCard = popularBadge.locator("..");
|
||||
// The card should have ring-2 and shadow-brand classes (or similar visual distinction)
|
||||
const cardClasses = await popularCard.getAttribute("class");
|
||||
expect(cardClasses).toBeTruthy();
|
||||
// At minimum it should have some border or ring
|
||||
const hasVisualDistinction =
|
||||
cardClasses?.includes("ring") ||
|
||||
cardClasses?.includes("shadow-brand") ||
|
||||
cardClasses?.includes("primary");
|
||||
expect(hasVisualDistinction).toBe(true);
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-2-popular-plan.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
// ── P0-3: CTA button text progression (Plan Seç → Devam Et) ─────────────
|
||||
test("P0-3: CTA button text progression", async ({ page }) => {
|
||||
// Initially, plan cards show "Plan Seç" buttons
|
||||
const choosePlanButtons = page.getByRole("button", {
|
||||
name: /plan seç|choose plan/i,
|
||||
});
|
||||
const initialCount = await choosePlanButtons.count();
|
||||
expect(initialCount).toBeGreaterThan(0);
|
||||
|
||||
// Click "Plan Seç" on any plan (not the current plan)
|
||||
// We need to find a plan we're NOT currently subscribed to
|
||||
const unsubscribedPlanBtn = choosePlanButtons.first();
|
||||
await unsubscribedPlanBtn.click();
|
||||
|
||||
// After selection, the button text should change to "Devam Et" (proceed)
|
||||
// and the sticky checkout bar should appear
|
||||
const proceedBtn = page.getByRole("button", { name: /devam et|proceed/i });
|
||||
await expect(proceedBtn.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-3-cta-progression.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
// ── P0-4: Order summary card visible before payment ─────────────────────
|
||||
test("P0-4: Order summary card visible before payment", async ({ page }) => {
|
||||
// Select a plan first (not the current one)
|
||||
const choosePlanBtns = page.getByRole("button", {
|
||||
name: /plan seç|choose plan/i,
|
||||
});
|
||||
const btnCount = await choosePlanBtns.count();
|
||||
if (btnCount > 0) {
|
||||
await choosePlanBtns.first().click();
|
||||
}
|
||||
|
||||
// Order summary card should appear
|
||||
const orderSummary = page.getByText(/sipariş özeti|order summary/i);
|
||||
await expect(orderSummary).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify key elements inside the summary:
|
||||
// - Plan name
|
||||
// - Period (monthly/yearly)
|
||||
// - Price with TRY symbol
|
||||
const planName = page.getByText(/marka|full paket/i, { exact: false });
|
||||
await expect(planName.first()).toBeVisible();
|
||||
|
||||
const periodText = page.getByText(/aylık|monthly|yıllık|yearly/i);
|
||||
await expect(periodText.first()).toBeVisible();
|
||||
|
||||
const priceText = page.getByText(/₺/);
|
||||
await expect(priceText.first()).toBeVisible();
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-4-order-summary.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
// ── P0-5: Current plan shows non-interactive Mevcut Plan badge ─────────
|
||||
test("P0-5: Current plan shows non-interactive Mevcut Plan badge", async ({ page }) => {
|
||||
// As admin with active subscription, the current plan card should show
|
||||
// "Mevcut Plan" badge and have non-interactive content (no "Plan Seç" button)
|
||||
const currentPlanBadge = page.getByText(/mevcut plan|current plan/i);
|
||||
await expect(currentPlanBadge.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// The current plan card should NOT have a "Plan Seç" button —
|
||||
// it should show a non-interactive indicator instead
|
||||
const currentPlanParent = currentPlanBadge.first().locator("..");
|
||||
const planSecBtn = currentPlanParent.getByRole("button", {
|
||||
name: /plan seç/i,
|
||||
});
|
||||
await expect(planSecBtn).not.toBeVisible();
|
||||
|
||||
// The "Mevcut Plan" indicator should show a check icon
|
||||
const currentPlanSection = page.locator(
|
||||
'[class*="green"]:has-text("Mevcut Plan"), [class*="green"]:has-text("Current")',
|
||||
);
|
||||
const isVisible = await currentPlanSection.first().isVisible().catch(() => false);
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-5-current-plan.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
// ── P0-6: Trial CTA hidden during active subscription ──────────────────
|
||||
test("P0-6: Trial CTA hidden during active subscription", async ({ page }) => {
|
||||
// When user has an active subscription (admin does), the trial CTA card
|
||||
// ("Ücretsiz Denemeyi Başlat" / "Start Free Trial") should NOT be visible
|
||||
const trialCTA = page.getByRole("button", {
|
||||
name: /ücretsiz deneme|start trial|start free trial/i,
|
||||
});
|
||||
await expect(trialCTA).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// The active subscription status card should be visible instead
|
||||
const statusBadge = page.getByText(/aktif|active/i);
|
||||
await expect(statusBadge.first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-6-no-trial-cta.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
// ── P0-7: Trust copy present on subscription page ─────────────────────
|
||||
test("P0-7: Trust copy present (SSL, provider, KVKK)", async ({ page }) => {
|
||||
// Select a plan to reveal the trust copy section near the proceed button
|
||||
const choosePlanBtns = page.getByRole("button", {
|
||||
name: /plan seç|choose plan/i,
|
||||
});
|
||||
const btnCount = await choosePlanBtns.count();
|
||||
if (btnCount > 0) {
|
||||
await choosePlanBtns.first().click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
// After selecting a plan, verify trust copy section is visible.
|
||||
// The trust copy is rendered as an aria-label="Ödeme güvencesi" section
|
||||
// containing SSL, provider (iyzico), and KVKK link
|
||||
const trustSection = page.locator('[aria-label="Ödeme güvencesi"]');
|
||||
// If aria-label is not matched, try text-based approach
|
||||
const sslText = page.getByText(/256-bit SSL|SSL/i);
|
||||
const providerText = page.getByText(/iyzico/i);
|
||||
const kvkkLink = page.getByRole("link", { name: /kvkk/i });
|
||||
|
||||
let trustFound = false;
|
||||
|
||||
// Check if aria-label section exists
|
||||
if (await trustSection.isVisible().catch(() => false)) {
|
||||
trustFound = true;
|
||||
}
|
||||
|
||||
// Check individual trust elements
|
||||
const sslVisible = await sslText.first().isVisible().catch(() => false);
|
||||
const providerVisible = await providerText.first().isVisible().catch(() => false);
|
||||
const kvkkVisible = await kvkkLink.isVisible().catch(() => false);
|
||||
|
||||
if (sslVisible || providerVisible || kvkkVisible) {
|
||||
trustFound = true;
|
||||
}
|
||||
|
||||
expect(trustFound).toBe(true);
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-7-trust-copy.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
// ── P0-8: Skeleton loading states correct ──────────────────────────────
|
||||
test("P0-8: Skeleton loading states correct", async ({ page }) => {
|
||||
// Navigate fresh (no cache) to trigger skeleton loading
|
||||
await page.goto("/dashboard/subscription", { waitUntil: "commit" });
|
||||
|
||||
// Capture skeleton state immediately after navigation starts
|
||||
// Skeletons have the animate-pulse class and are rendered by @sase/ui Skeleton
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-8-skeleton-loading.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// Look for skeleton elements — Skeleton component from @sase/ui
|
||||
// renders div with animate-pulse class
|
||||
const skeletonElements = page.locator(".animate-pulse");
|
||||
|
||||
// Either skeletons are visible during loading, or page loads too fast
|
||||
// In either case, we verify skeletons exist in the DOM at some point
|
||||
// or the final content renders correctly
|
||||
const skeletonCount = await skeletonElements.count();
|
||||
// After loading completes, we should have the heading visible
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /abonelik|subscription/i }),
|
||||
).toBeVisible({ timeout: 30000 });
|
||||
|
||||
// Skeletons should be gone after load
|
||||
const postLoadSkeletonCount = await skeletonElements.count();
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-8-after-load.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// After full load, skeletons should be zero
|
||||
expect(postLoadSkeletonCount).toBe(0);
|
||||
});
|
||||
|
||||
// ── P0-9: No hardcoded Turkish strings ──────────────────────────────────
|
||||
test("P0-9: No hardcoded Turkish strings in source (structural)", async ({
|
||||
page,
|
||||
}) => {
|
||||
// This test verifies that the rendered page uses i18n-based text.
|
||||
// We check that key subscription strings appear on the page —
|
||||
// confirming that the i18n translation system is working.
|
||||
|
||||
// These keys are defined in tr.json → they should render as Turkish text
|
||||
// via t("subscription.choosePlan"), t("subscription.currentPlan"), etc.
|
||||
const expectedI18nTexts = [
|
||||
"Plan Seç", // subscription.choosePlan
|
||||
"Mevcut Plan", // subscription.currentPlan
|
||||
"Popüler", // subscription.popular
|
||||
"Plan Karşılaştırması", // subscription.planComparison
|
||||
"Özellik Karşılaştırması", // subscription.featureMatrix
|
||||
];
|
||||
|
||||
// Get all visible text on the page
|
||||
const pageText = await page.locator("body").innerText();
|
||||
|
||||
for (const text of expectedI18nTexts) {
|
||||
expect(pageText).toContain(text);
|
||||
}
|
||||
|
||||
// Check for the English i18n texts when English is selected
|
||||
// (tr.json should have corresponding en.json entries)
|
||||
// This is a structural check — the i18n system renders the correct language
|
||||
});
|
||||
|
||||
// ── P0-10: PostHog events firing ───────────────────────────────────────
|
||||
test("P0-10: PostHog events firing", async ({ page }) => {
|
||||
// Collect PostHog network requests
|
||||
const posthogRequests: string[] = [];
|
||||
|
||||
page.on("request", (request) => {
|
||||
const url = request.url();
|
||||
if (url.includes("posthog") || url.includes("ph_log")) {
|
||||
posthogRequests.push(url);
|
||||
}
|
||||
});
|
||||
|
||||
// Interact with plan selection to trigger PostHog events
|
||||
const choosePlanBtns = page.getByRole("button", {
|
||||
name: /plan seç|choose plan/i,
|
||||
});
|
||||
const btnCount = await choosePlanBtns.count();
|
||||
if (btnCount > 0) {
|
||||
await choosePlanBtns.first().click();
|
||||
}
|
||||
|
||||
// Wait for network activity to settle
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Also click the billing period toggle to trigger more events
|
||||
const yearlyBtn = page.getByRole("button", { name: /yıllık|yearly/i });
|
||||
if (await yearlyBtn.isVisible().catch(() => false)) {
|
||||
await yearlyBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
await page.screenshot({
|
||||
path: `${SCREENSHOT_DIR}/p0-10-posthog-events.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// PostHog should have captured at least some events
|
||||
// PostHog may batch requests, so we may not see individual calls
|
||||
// but we verify the posthog/analytics library loaded
|
||||
const posthogLoaded = await page.evaluate(() => {
|
||||
return typeof (window as any).posthog !== "undefined";
|
||||
});
|
||||
|
||||
// Either posthog loaded or we captured network requests
|
||||
expect(posthogLoaded || posthogRequests.length > 0).toBe(true);
|
||||
|
||||
// Log what PostHog events we captured
|
||||
if (posthogRequests.length > 0) {
|
||||
console.log(`Captured ${posthogRequests.length} PostHog requests`);
|
||||
} else if (posthogLoaded) {
|
||||
console.log("PostHog client loaded (may be batching requests)");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: ".",
|
||||
testMatch: "**/*.spec.ts",
|
||||
timeout: 120_000,
|
||||
expect: { timeout: 15_000 },
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
fullyParallel: false,
|
||||
|
||||
use: {
|
||||
baseURL: "https://sase.tr",
|
||||
locale: "tr-TR",
|
||||
timezoneId: "Europe/Istanbul",
|
||||
screenshot: "on",
|
||||
trace: "retain-on-failure",
|
||||
video: "retain-on-failure",
|
||||
actionTimeout: 15_000,
|
||||
navigationTimeout: 30_000,
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "setup",
|
||||
testMatch: "auth.setup.ts",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
use: {
|
||||
...devices["Desktop Firefox"],
|
||||
storageState: ".auth/admin.json",
|
||||
},
|
||||
dependencies: ["setup"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"timestamp": "2026-05-13T05:56:14.809Z",
|
||||
"target": "https://sase.tr",
|
||||
"bundleHash": "index-BKd5Du15.js",
|
||||
"summary": {
|
||||
"sourceKeys": 45,
|
||||
"deployedKeys": 29,
|
||||
"missingKeys": 16
|
||||
},
|
||||
"p0Impact": [
|
||||
{
|
||||
"scenario": "P0-1 (Yearly discount badge)",
|
||||
"keys": [
|
||||
"yearlyDiscount"
|
||||
],
|
||||
"missing": [
|
||||
"yearlyDiscount"
|
||||
],
|
||||
"status": "FAIL"
|
||||
},
|
||||
{
|
||||
"scenario": "P0-3 (CTA text progression Plan Seç → Devam Et)",
|
||||
"keys": [
|
||||
"proceed"
|
||||
],
|
||||
"missing": [
|
||||
"proceed"
|
||||
],
|
||||
"status": "FAIL"
|
||||
},
|
||||
{
|
||||
"scenario": "P0-4 (Order summary card)",
|
||||
"keys": [
|
||||
"orderSummary",
|
||||
"orderSummaryPlan",
|
||||
"orderSummaryPeriod",
|
||||
"orderSummaryBrands",
|
||||
"orderSummaryPrice"
|
||||
],
|
||||
"missing": [
|
||||
"orderSummary",
|
||||
"orderSummaryPlan",
|
||||
"orderSummaryPeriod",
|
||||
"orderSummaryBrands",
|
||||
"orderSummaryPrice"
|
||||
],
|
||||
"status": "FAIL"
|
||||
},
|
||||
{
|
||||
"scenario": "P0-7 (Trust copy: SSL, iyzico, KVKK)",
|
||||
"keys": [
|
||||
"paymentTrustSSL",
|
||||
"paymentTrustProvider",
|
||||
"paymentTrustKVKK"
|
||||
],
|
||||
"missing": [
|
||||
"paymentTrustSSL",
|
||||
"paymentTrustProvider",
|
||||
"paymentTrustKVKK"
|
||||
],
|
||||
"status": "FAIL"
|
||||
},
|
||||
{
|
||||
"scenario": "P0-9 (No hardcoded strings / i18n coverage)",
|
||||
"keys": [
|
||||
"featureMatrix",
|
||||
"daysLeft",
|
||||
"trialDaysLeft",
|
||||
"trustNoCard",
|
||||
"trustCancelAnytime",
|
||||
"trustRefund"
|
||||
],
|
||||
"missing": [
|
||||
"featureMatrix",
|
||||
"daysLeft",
|
||||
"trialDaysLeft",
|
||||
"trustNoCard",
|
||||
"trustCancelAnytime",
|
||||
"trustRefund"
|
||||
],
|
||||
"status": "FAIL"
|
||||
}
|
||||
],
|
||||
"missingKeys": [
|
||||
"featureMatrix",
|
||||
"yearlyDiscount",
|
||||
"proceed",
|
||||
"orderSummary",
|
||||
"orderSummaryPlan",
|
||||
"orderSummaryPeriod",
|
||||
"orderSummaryBrands",
|
||||
"orderSummaryPrice",
|
||||
"daysLeft",
|
||||
"trustNoCard",
|
||||
"trustCancelAnytime",
|
||||
"trustRefund",
|
||||
"paymentTrustSSL",
|
||||
"paymentTrustProvider",
|
||||
"paymentTrustKVKK",
|
||||
"trialDaysLeft"
|
||||
],
|
||||
"verifiedPresent": [
|
||||
"P0-2",
|
||||
"P0-5",
|
||||
"P0-6",
|
||||
"P0-8",
|
||||
"P0-10"
|
||||
]
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"timestamp": "2026-05-13T05:56:14.810Z",
|
||||
"method": "Bundle-level verification (no browser automation available — sandbox lacks GUI libraries)",
|
||||
"target": "https://sase.tr",
|
||||
"testAccount": "admin@sase.tr",
|
||||
"results": [
|
||||
{
|
||||
"id": "P0-1",
|
||||
"name": "Yearly discount badge",
|
||||
"passed": false,
|
||||
"reason": "yearlyDiscount i18n key missing from deployed bundle"
|
||||
},
|
||||
{
|
||||
"id": "P0-2",
|
||||
"name": "Popular plan visual distinction",
|
||||
"passed": true,
|
||||
"reason": "ring-2/shadow-brand/lg CSS classes present in deployed bundle"
|
||||
},
|
||||
{
|
||||
"id": "P0-3",
|
||||
"name": "CTA button text progression",
|
||||
"passed": false,
|
||||
"reason": "proceed i18n key missing; deployed code uses 'subscribe' not 'proceed'"
|
||||
},
|
||||
{
|
||||
"id": "P0-4",
|
||||
"name": "Order summary card",
|
||||
"passed": false,
|
||||
"reason": "orderSummary* i18n keys missing from deployed bundle"
|
||||
},
|
||||
{
|
||||
"id": "P0-5",
|
||||
"name": "Current plan Mevcut Plan badge",
|
||||
"passed": true,
|
||||
"reason": "currentPlan i18n key present, green styling classes in CSS"
|
||||
},
|
||||
{
|
||||
"id": "P0-6",
|
||||
"name": "Trial CTA hidden during active sub",
|
||||
"passed": true,
|
||||
"reason": "eligibleForTrial conditional logic present in deployed code"
|
||||
},
|
||||
{
|
||||
"id": "P0-7",
|
||||
"name": "Trust copy (SSL, iyzico, KVKK)",
|
||||
"passed": false,
|
||||
"reason": "paymentTrust* i18n keys missing; SSL/KVKK from other contexts only"
|
||||
},
|
||||
{
|
||||
"id": "P0-8",
|
||||
"name": "Skeleton loading states",
|
||||
"passed": true,
|
||||
"reason": "animate-pulse and Skeleton component present in deployed bundle"
|
||||
},
|
||||
{
|
||||
"id": "P0-9",
|
||||
"name": "No hardcoded Turkish strings",
|
||||
"passed": false,
|
||||
"reason": "16 i18n keys missing — t() fallback shows raw key names"
|
||||
},
|
||||
{
|
||||
"id": "P0-10",
|
||||
"name": "PostHog events firing",
|
||||
"passed": true,
|
||||
"reason": "PostHog capture events (plan_selected, checkout_started, etc.) present in bundle"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 10,
|
||||
"passed": 5,
|
||||
"failed": 5
|
||||
},
|
||||
"criticalFinding": "The deployed sase.tr bundle (index-BKd5Du15.js) is missing 16 subscription i18n translation keys that exist in the source code (apps/web/src/messages/tr.json). This means the P0 CRO fixes exist in the repository source code but have NOT been deployed to production. A redeployment is required."
|
||||
}
|
||||
@@ -767,7 +767,7 @@ export function SubscriptionPage() {
|
||||
|
||||
{/* Downgrade Offer Dialog */}
|
||||
<Dialog open={downgradeOfferOpen} onOpenChange={setDowngradeOfferOpen}>
|
||||
<DialogContent className="max-w-md sm:max-w-lg">
|
||||
<DialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("subscription.downgradeOffer.title")}</DialogTitle>
|
||||
<DialogDescription>{t("subscription.downgradeOffer.description")}</DialogDescription>
|
||||
|
||||
@@ -1,631 +0,0 @@
|
||||
# PostHog Funnel Monitoring — P0 Subscription CRO Post-Deploy
|
||||
|
||||
**Date:** 2026-05-13 (deployment day) — monitoring window: 48–72 hours
|
||||
**Trigger:** FN-298 deployed P0-1 through P0-10 subscription CRO fixes to production (sase.tr)
|
||||
**Status:** 🔴 MONITORING ACTIVE — Day 0 (partial, deployment afternoon)
|
||||
**Owner:** CPO / Product
|
||||
**Parallel task:** FN-309 (QA post-deploy visual verification — PASS, May 13)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
On 2026-05-13, 10 P0 subscription CRO fixes were deployed to production via FN-298 (re-deploy after initial stale-bundle incident). All fixes are visually verified PASS by FN-309 and FN-320 Playwright verification. This document tracks PostHog funnel metrics for 48–72 hours post-deploy to measure conversion impact.
|
||||
|
||||
**Fixes deployed:**
|
||||
- P0-1: Pricing card redesign with 17% yearly discount badge
|
||||
- P0-2: Feature matrix comparison on plan cards
|
||||
- P0-3: Trial urgency banner (≤3 days remaining)
|
||||
- P0-4: Order summary card after plan selection
|
||||
- P0-5: Cancel-save flow with downgrade offer dialog
|
||||
- P0-6: Sticky checkout bar on scroll
|
||||
- P0-7: Payment trust copy (256-bit SSL + Iyzico + KVKK)
|
||||
- P0-8: Core trust badges (no card, cancel anytime, 7-day refund)
|
||||
- P0-9: Social proof strip (landing + auth + subscription)
|
||||
- P0-10: Yearly billing toggle analytics
|
||||
|
||||
**All fixes are instrumented with PostHog events** — see Event Inventory below.
|
||||
|
||||
---
|
||||
|
||||
## 1. Event Inventory
|
||||
|
||||
### 1.1 Funnel Events (Instrumented)
|
||||
|
||||
All events verified present in production code as of FN-298 deploy:
|
||||
|
||||
| Event | Properties | Location | Added By |
|
||||
|-------|-----------|----------|----------|
|
||||
| `plan_selected` | `plan` (key) | `subscription/index.tsx:327` | Pre-existing |
|
||||
| `checkout_started` | `plan`, `period` | `subscription/index.tsx:345` | Pre-existing |
|
||||
| `payment_initiated` | `method`, `plan`, `period`, `amount` | `payment-content.tsx:196,202` | Pre-existing |
|
||||
| `payment_success` | `method`, `plan`, `period`, `amount`¹ | `payment-content.tsx:73,135` | FN-281 |
|
||||
| `payment_failed` | `method`, `plan`, `period`, `reason`¹ | `payment-content.tsx:76` | FN-281 |
|
||||
| `receipt_uploaded` | `payment_id` | `payment-content.tsx:209` | Pre-existing |
|
||||
|
||||
¹ Backend also captures `payment_success`/`payment_failed` in `payments.service.ts` with `method`, `payment_id`, `subscription_id`, `amount` (but not `plan`/`period` — property gap tracked in FN-321).
|
||||
|
||||
### 1.2 P0 CRO Fix Events (Newly Instrumented)
|
||||
|
||||
| Event | Properties | Location | P0 Fix |
|
||||
|-------|-----------|----------|--------|
|
||||
| `yearly_toggle_clicked` | `period` ("yearly"\|"monthly") | `subscription/index.tsx:1055,1066` | P0-1/P0-10 |
|
||||
| `trial_urgency_banner_viewed` | `trial_end_date`, `days_remaining` | `trial-urgency-banner.tsx:77` | P0-3 |
|
||||
| `trial_urgency_banner_cta_clicked` | `trial_end_date`, `days_remaining` | `trial-urgency-banner.tsx:97` | P0-3 |
|
||||
| `trial_urgency_banner_dismissed` | `trial_end_date`, `days_remaining` | `trial-urgency-banner.tsx:87` | P0-3 |
|
||||
| `downgrade_offer_shown` | `from_plan`, `to_plan` | `subscription/index.tsx:365` | P0-5 |
|
||||
| `downgrade_offer_accepted` | `from_plan`, `to_plan` | `subscription/index.tsx:265` | P0-5 |
|
||||
| `downgrade_offer_declined` | `from_plan` | `subscription/index.tsx:843` | P0-5 |
|
||||
| `cancel_flow_viewed` | (none) | `subscription/index.tsx:892` | P0-5 |
|
||||
| `cancel_save_clicked` | (none) | `subscription/index.tsx:269` | P0-5 |
|
||||
| `social_proof_impression` | `page`, `section` | `subscription/index.tsx:457,474`; `index.tsx:573,590,607`; `_auth.tsx:23` | P0-9 (FN-282, FN-297) |
|
||||
| `social_proof_engaged` | `page`, `section` | `subscription/index.tsx:484,488`; `index.tsx:616`; `_auth.tsx:33` | P0-9 (FN-282, FN-297) |
|
||||
|
||||
### 1.3 Pageview Proxy for `subscription_page_viewed`
|
||||
|
||||
No explicit `subscription_page_viewed` event exists. Instead, we filter `$pageview` events where `$current_url` contains `/dashboard/subscription`. The root route (`__root.tsx:95`) captures `$pageview` on every navigation with `$current_url` set to `window.location.origin + location.pathname`.
|
||||
|
||||
**PostHog filter:** `$pageview` with `$current_url` containing `/dashboard/subscription` (but NOT `/dashboard/subscription/pay`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Metrics & Success Criteria
|
||||
|
||||
### 2.1 Primary Metric: Checkout Conversion
|
||||
|
||||
**Funnel:** `checkout_started → payment_initiated`
|
||||
**Baseline period:** May 6–12, 2026 (7 days pre-deploy)
|
||||
**Post period:** May 13–15/16, 2026 (48–72 hours post-deploy)
|
||||
|
||||
| Criterion | Threshold |
|
||||
|-----------|-----------|
|
||||
| ✅ Success | ≥3pp improvement in conversion rate |
|
||||
| ⚠️ Acceptable | ±0–3pp (no regression) |
|
||||
| 🔴 Kill | >5pp DROP from baseline → escalate to CEO as P0 |
|
||||
|
||||
### 2.2 Secondary Metric: Subscription Page Abandonment
|
||||
|
||||
**Funnel proxy:** `$pageview` (filtered to `/dashboard/subscription`) → `checkout_started`
|
||||
**What it measures:** Did trust elements (social proof, trust badges, payment trust copy) reduce abandonment between viewing the subscription page and initiating checkout?
|
||||
|
||||
| Criterion | Threshold |
|
||||
|-----------|-----------|
|
||||
| ✅ Success | No regression (at or above baseline) |
|
||||
| 🔴 Concern | Any decrease >5pp warrants investigation |
|
||||
|
||||
### 2.3 Cohort Analysis: New vs Returning Visitors
|
||||
|
||||
**Hypothesis:** Trial urgency banner (`trial_urgency_banner_viewed`) should increase `checkout_started` rate among users with active trials (returning visitors) without deterring new visitors.
|
||||
|
||||
**PostHog breakdown:** Split `checkout_started → payment_initiated` funnel by:
|
||||
- New visitors (first session within window)
|
||||
- Returning visitors (existing user with prior session)
|
||||
|
||||
### 2.4 Tertiary Metric: Downgrade Offer Flow
|
||||
|
||||
**Funnel:** `downgrade_offer_shown → downgrade_offer_accepted`
|
||||
**What it measures:** Cancel-save flow effectiveness — of users who see the downgrade offer, what percentage accept it vs cancel outright?
|
||||
|
||||
**Note:** Base for this funnel may be very small (only fires when user with multi-plan subscription initiates cancel). Treat as directional, not statistically significant.
|
||||
|
||||
### 2.5 Full End-to-End Funnel (New)
|
||||
|
||||
**Funnel:** `checkout_started → payment_initiated → payment_success`
|
||||
**Enabled by:** FN-281 (`d855b56`) — `payment_success` now live
|
||||
**Target:** `payment_success / payment_initiated` ≥ 70% (per CPO brief, `docs/product/cpo-brief-payment-events.md`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Pre-Deploy Baseline (May 6–12, 2026)
|
||||
|
||||
### 3.1 PostHog Query Configuration
|
||||
|
||||
To reproduce these numbers in the PostHog dashboard:
|
||||
|
||||
```
|
||||
Product analytics → Funnels → New funnel
|
||||
|
||||
Funnel name: P0 CRO — Checkout Baseline (Pre)
|
||||
Steps:
|
||||
1. checkout_started
|
||||
2. payment_initiated
|
||||
|
||||
Date range: May 6, 2026 → May 12, 2026 (inclusive)
|
||||
Conversion window: 1 hour
|
||||
|
||||
Breakdown: plan (property)
|
||||
```
|
||||
|
||||
```
|
||||
Funnel name: P0 CRO — Full Funnel Baseline (Pre)
|
||||
Steps:
|
||||
1. checkout_started
|
||||
2. payment_initiated
|
||||
3. payment_success
|
||||
|
||||
Date range: May 6, 2026 → May 12, 2026
|
||||
Conversion window: 24 hours
|
||||
```
|
||||
|
||||
```
|
||||
Funnel name: P0 CRO — Subscription → Checkout Baseline (Pre)
|
||||
Steps:
|
||||
1. $pageview (filter: $current_url contains /dashboard/subscription, excludes /pay)
|
||||
2. checkout_started
|
||||
|
||||
Date range: May 6, 2026 → May 12, 2026
|
||||
Conversion window: 1 hour
|
||||
```
|
||||
|
||||
```
|
||||
Funnel name: P0 CRO — Downgrade Offer Baseline (Pre)
|
||||
Steps:
|
||||
1. downgrade_offer_shown
|
||||
2. downgrade_offer_accepted
|
||||
|
||||
Date range: May 6, 2026 → May 12, 2026
|
||||
Conversion window: 1 hour
|
||||
```
|
||||
|
||||
### 3.2 Baseline Data (COLLECT MANUALLY FROM POSTHOG)
|
||||
|
||||
> ⚠️ **Action required:** Log into PostHog (eu.posthog.com, project 127747) and run the queries above. Fill in the tables below.
|
||||
|
||||
**Table 3A: Primary Funnel — checkout_started → payment_initiated**
|
||||
|
||||
| Date | checkout_started | payment_initiated | Conversion Rate |
|
||||
|------|-----------------|-------------------|----------------|
|
||||
| May 6 | TBD | TBD | TBD% |
|
||||
| May 7 | TBD | TBD | TBD% |
|
||||
| May 8 | TBD | TBD | TBD% |
|
||||
| May 9 | TBD | TBD | TBD% |
|
||||
| May 10 | TBD | TBD | TBD% |
|
||||
| May 11 | TBD | TBD | TBD% |
|
||||
| May 12 | TBD | TBD | TBD% |
|
||||
| **7-day Total** | **TBD** | **TBD** | **TBD%** ← BASELINE |
|
||||
|
||||
**Table 3B: Secondary Funnel — subscription_page → checkout_started**
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| subscription_page_viewed ($pageview proxy) | TBD |
|
||||
| checkout_started | TBD |
|
||||
| Conversion Rate | TBD% ← BASELINE |
|
||||
|
||||
**Table 3C: Full End-to-End Funnel — checkout_started → payment_initiated → payment_success**
|
||||
|
||||
| Step | Count | Drop-off | Conversion (cumulative) |
|
||||
|------|-------|----------|------------------------|
|
||||
| checkout_started | TBD | — | 100% |
|
||||
| payment_initiated | TBD | TBD% | TBD% |
|
||||
| payment_success | TBD | TBD% | TBD% |
|
||||
|
||||
**Table 3D: Downgrade Offer Flow**
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| downgrade_offer_shown | TBD |
|
||||
| downgrade_offer_accepted | TBD |
|
||||
| Save Rate | TBD% |
|
||||
|
||||
### 3.3 Baseline Plan Tier Breakdown
|
||||
|
||||
| Plan | checkout_started | payment_initiated | Conversion Rate |
|
||||
|------|-----------------|-------------------|----------------|
|
||||
| brand1 | TBD | TBD | TBD% |
|
||||
| brand2 | TBD | TBD | TBD% |
|
||||
| brand3 | TBD | TBD | TBD% |
|
||||
| brand5 | TBD | TBD | TBD% |
|
||||
| full | TBD | TBD | TBD% |
|
||||
|
||||
---
|
||||
|
||||
## 4. Post-Deploy Daily Snapshots
|
||||
|
||||
### Day 1 — May 13, 2026 (Deployment Day, Partial)
|
||||
|
||||
**Deploy time:** ~midday (exact timestamp TBD from deployment logs)
|
||||
**Data coverage:** Partial day (~12 hours post-deploy)
|
||||
**Caveats:** Deployment-day data includes pre-deploy morning traffic. Numbers are directional only.
|
||||
|
||||
**Table 4A: Primary Funnel (May 13)**
|
||||
|
||||
| Metric | Value | vs Baseline (TBD%) |
|
||||
|--------|-------|---------------------|
|
||||
| checkout_started | TBD | — |
|
||||
| payment_initiated | TBD | — |
|
||||
| Conversion Rate | TBD% | TBDpp |
|
||||
|
||||
**Table 4B: Secondary Funnel (May 13)**
|
||||
|
||||
| Metric | Value | vs Baseline (TBD%) |
|
||||
|--------|-------|---------------------|
|
||||
| subscription_page → checkout_started rate | TBD% | TBDpp |
|
||||
|
||||
**Table 4C: Full Funnel (May 13)**
|
||||
|
||||
| Step | Count | Rate |
|
||||
|------|-------|------|
|
||||
| checkout_started | TBD | 100% |
|
||||
| payment_initiated | TBD | TBD% |
|
||||
| payment_success | TBD | TBD% |
|
||||
|
||||
**Table 4D: Trial Urgency Banner (May 13)**
|
||||
|
||||
| Event | Count |
|
||||
|-------|-------|
|
||||
| trial_urgency_banner_viewed | TBD |
|
||||
| trial_urgency_banner_cta_clicked | TBD |
|
||||
| trial_urgency_banner_dismissed | TBD |
|
||||
| CTA Click Rate (cta/viewed) | TBD% |
|
||||
|
||||
**Table 4E: Yearly Toggle (May 13)**
|
||||
|
||||
| Event | Count |
|
||||
|-------|-------|
|
||||
| yearly_toggle_clicked (→ yearly) | TBD |
|
||||
| yearly_toggle_clicked (→ monthly) | TBD |
|
||||
|
||||
**Table 4F: Kill Criteria Check (May 13)**
|
||||
|
||||
| Metric | Baseline | May 13 | Δ | Threshold | Status |
|
||||
|--------|----------|--------|---|-----------|--------|
|
||||
| checkout → payment_initiated | TBD% | TBD% | TBDpp | > -5pp | ⏳ PENDING |
|
||||
|
||||
---
|
||||
|
||||
### Day 2 — May 14, 2026 (First Full Day Post-Deploy)
|
||||
|
||||
**Data coverage:** Full 24 hours
|
||||
**Caveats:** First full day of data. Weekend patterns may differ (May 14 is a Wednesday — weekday, normal traffic).
|
||||
|
||||
**Table 5A: Primary Funnel (May 14)**
|
||||
|
||||
| Metric | Value | vs Baseline (TBD%) |
|
||||
|--------|-------|---------------------|
|
||||
| checkout_started | TBD | — |
|
||||
| payment_initiated | TBD | — |
|
||||
| Conversion Rate | TBD% | TBDpp |
|
||||
|
||||
**Table 5B: Secondary Funnel (May 14)**
|
||||
|
||||
| Metric | Value | vs Baseline (TBD%) |
|
||||
|--------|-------|---------------------|
|
||||
| subscription_page → checkout_started rate | TBD% | TBDpp |
|
||||
|
||||
**Table 5C: Full Funnel (May 14)**
|
||||
|
||||
| Step | Count | Rate |
|
||||
|------|-------|------|
|
||||
| checkout_started | TBD | 100% |
|
||||
| payment_initiated | TBD | TBD% |
|
||||
| payment_success | TBD | TBD% |
|
||||
|
||||
**Table 5D: Kill Criteria Check (May 14)**
|
||||
|
||||
| Metric | Baseline | May 14 | Δ | Threshold | Status |
|
||||
|--------|----------|--------|---|-----------|--------|
|
||||
| checkout → payment_initiated | TBD% | TBD% | TBDpp | > -5pp | ⏳ PENDING |
|
||||
|
||||
---
|
||||
|
||||
### Day 3 — May 15, 2026 (48-Hour Mark)
|
||||
|
||||
**Data coverage:** Full 24 hours
|
||||
**Significance:** 48-hour kill criterion window closes. If no regression >5pp is detected, P0 escalation threshold is cleared.
|
||||
|
||||
**Table 6A: Primary Funnel (May 15)**
|
||||
|
||||
| Metric | Value | vs Baseline (TBD%) |
|
||||
|--------|-------|---------------------|
|
||||
| checkout_started | TBD | — |
|
||||
| payment_initiated | TBD | — |
|
||||
| Conversion Rate | TBD% | TBDpp |
|
||||
|
||||
**Table 6B: Secondary Funnel (May 15)**
|
||||
|
||||
| Metric | Value | vs Baseline (TBD%) |
|
||||
|--------|-------|---------------------|
|
||||
| subscription_page → checkout_started rate | TBD% | TBDpp |
|
||||
|
||||
**Table 6C: Full Funnel (May 15)**
|
||||
|
||||
| Step | Count | Rate |
|
||||
|------|-------|------|
|
||||
| checkout_started | TBD | 100% |
|
||||
| payment_initiated | TBD | TBD% |
|
||||
| payment_success | TBD | TBD% |
|
||||
|
||||
**Table 6D: Downgrade Offer Flow (May 13–15 Cumulative)**
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| downgrade_offer_shown | TBD |
|
||||
| downgrade_offer_accepted | TBD |
|
||||
| downgrade_offer_declined | TBD |
|
||||
| Save Rate (accepted / shown) | TBD% |
|
||||
|
||||
**Table 6E: Kill Criteria Check (May 15)**
|
||||
|
||||
| Metric | Baseline | May 13-15 Aggregate | Δ | Threshold | Status |
|
||||
|--------|----------|---------------------|---|-----------|--------|
|
||||
| checkout → payment_initiated | TBD% | TBD% | TBDpp | > -5pp | ⏳ PENDING |
|
||||
|
||||
---
|
||||
|
||||
### Day 4 (Optional) — May 16, 2026 (72-Hour Mark)
|
||||
|
||||
**Data coverage:** Full 24 hours
|
||||
**Significance:** Extended monitoring window. Provides 3 full days of post-deploy data. Better statistical power.
|
||||
|
||||
**Table 7A: Primary Funnel (May 16)**
|
||||
|
||||
| Metric | Value | vs Baseline (TBD%) |
|
||||
|--------|-------|---------------------|
|
||||
| checkout_started | TBD | — |
|
||||
| payment_initiated | TBD | — |
|
||||
| Conversion Rate | TBD% | TBDpp |
|
||||
|
||||
**Table 7B: 4-Day Aggregate (May 13–16)**
|
||||
|
||||
| Metric | Pre (May 6-12) | Post (May 13-16) | Δ |
|
||||
|--------|----------------|------------------|---|
|
||||
| checkout_started | TBD | TBD | TBD |
|
||||
| payment_initiated | TBD | TBD | TBD |
|
||||
| Conversion Rate | TBD% | TBD% | TBDpp |
|
||||
|
||||
---
|
||||
|
||||
## 5. Cohort Analysis (To Be Populated)
|
||||
|
||||
### 5.1 New vs Returning Visitors
|
||||
|
||||
PostHog query configuration:
|
||||
|
||||
```
|
||||
Product analytics → Funnels → New funnel
|
||||
|
||||
Funnel name: P0 CRO — Cohort: New Visitors
|
||||
Steps:
|
||||
1. checkout_started
|
||||
2. payment_initiated
|
||||
|
||||
Filter: Person properties → is_returning_user = false
|
||||
OR: First touch point within date range
|
||||
Date range: May 13 → May 16, 2026
|
||||
Conversion window: 1 hour
|
||||
```
|
||||
|
||||
```
|
||||
Funnel name: P0 CRO — Cohort: Returning Visitors
|
||||
Steps:
|
||||
1. checkout_started
|
||||
2. payment_initiated
|
||||
|
||||
Filter: Person properties → is_returning_user = true
|
||||
OR: Has session events before May 13, 2026
|
||||
Date range: May 13 → May 16, 2026
|
||||
Conversion window: 1 hour
|
||||
```
|
||||
|
||||
### 5.2 Cohort Data
|
||||
|
||||
| Cohort | checkout_started | payment_initiated | Conversion Rate | vs Baseline |
|
||||
|--------|-----------------|-------------------|----------------|-------------|
|
||||
| New Visitors | TBD | TBD | TBD% | TBDpp |
|
||||
| Returning Visitors | TBD | TBD | TBD% | TBDpp |
|
||||
|
||||
### 5.3 Cohort Interpretation Guide
|
||||
|
||||
- **Trial urgency banner hypothesis:** If returning visitors' `checkout_started` volume increases relative to new visitors, the urgency banner is likely the driver (returning users are more likely to be on trials).
|
||||
- **Trust elements hypothesis:** If new visitors' `subscription_page → checkout_started` improves, social proof and trust badges are reducing first-time abandonment.
|
||||
- **Yearly toggle hypothesis:** If `yearly_toggle_clicked (→ yearly)` volume is significant and `checkout_started` for yearly plans increases, the discount badge + toggle UX are converting monthly interest to annual commitment.
|
||||
|
||||
---
|
||||
|
||||
## 6. Kill Criteria Threshold Monitoring
|
||||
|
||||
### 6.1 Kill Criterion Definition
|
||||
|
||||
> If `payment_initiated / checkout_started` drops >5pp from 7-day rolling baseline within 48 hours, escalate to CEO as P0 — potential UX regression from the CRO changes.
|
||||
|
||||
### 6.2 Threshold Tracker
|
||||
|
||||
| Check Time | Baseline Rate | Current Rate | Δ | >5pp Drop? | Action |
|
||||
|------------|--------------|-------------|---|-----------|--------|
|
||||
| May 13 EOD | TBD% | TBD% | TBDpp | TBD | TBD |
|
||||
| May 14 EOD (24h) | TBD% | TBD% | TBDpp | TBD | TBD |
|
||||
| May 15 EOD (48h) | TBD% | TBD% | TBDpp | TBD | TBD |
|
||||
| May 16 EOD (72h) | TBD% | TBD% | TBDpp | TBD | TBD |
|
||||
|
||||
**Escalation protocol:**
|
||||
1. Verify the data is not a PostHog instrumentation issue (check SDK status, event volume, no dropped events)
|
||||
2. If confirmed regression >5pp: immediate CEO escalation with this document + PostHog funnel screenshot
|
||||
3. Concurrently: open rollback consideration — which fix(es) to revert first
|
||||
4. Within 4 hours of escalation: decision on rollback vs. hotfix vs. monitor longer
|
||||
|
||||
### 6.3 Regression Root-Cause Checklist
|
||||
|
||||
If conversion drops, investigate in order:
|
||||
|
||||
- [ ] **Payment gateway:** Iyzico uptime, callback success rate, `payment_failed` spike?
|
||||
- [ ] **UI bug:** Is a CTA broken/clipped/hidden? Check Playwright screenshot diff from FN-320.
|
||||
- [ ] **Sticky bar:** Is the sticky checkout bar overlapping content on mobile, hiding a different element?
|
||||
- [ ] **Trial banner:** Is the urgency banner alarming users or making the page feel broken?
|
||||
- [ ] **Pricing confusion:** Did the yearly discount badge make monthly plans look "wrong" or "not a deal"?
|
||||
- [ ] **Traffic shift:** Did a marketing campaign drive low-intent traffic that skews the funnel? Check `$referrer`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Final Verdict
|
||||
|
||||
> ⏳ **PENDING** — Monitoring window active. Verdict will be populated after 48–72 hours of data collection.
|
||||
|
||||
### 7.1 Verdict Framework
|
||||
|
||||
| Verdict | Criteria |
|
||||
|---------|----------|
|
||||
| ✅ **CLEAR WIN** | `payment_initiated / checkout_started` improved ≥3pp; no regression in secondary metrics; kill criterion not triggered |
|
||||
| ✅ **NEUTRAL** | Primary metric within ±3pp of baseline; no regression in secondary metrics |
|
||||
| ⚠️ **MINOR REGRESSION** | Primary metric dropped 1–5pp; investigation launched; no immediate rollback needed |
|
||||
| 🔴 **P0 ESCALATION** | Primary metric dropped >5pp within 48h; CEO notified; rollback evaluation initiated |
|
||||
| ⚠️ **INCONCLUSIVE** | Sample size too small for statistical significance; extend monitoring to 7 days |
|
||||
|
||||
### 7.2 Verdict by Fix (Directional)
|
||||
|
||||
| Fix | Metric | Pre Rate | Post Rate | Δ | Assessment |
|
||||
|-----|--------|----------|-----------|---|------------|
|
||||
| P0-1/P0-10 | yearly_toggle_clicked volume + yearly checkout_started | TBD | TBD | TBD | ⏳ |
|
||||
| P0-2 | Feature matrix impact (indirect — via checkout_started) | TBD | TBD | TBD | ⏳ |
|
||||
| P0-3 | Trial urgency banner: cta_clicked/viewed rate | TBD% | TBD% | TBDpp | ⏳ |
|
||||
| P0-4 | Order summary impact (indirect — via checkout_started) | TBD | TBD | TBD | ⏳ |
|
||||
| P0-5 | Downgrade offer save rate (accepted/shown) | TBD% | TBD% | TBDpp | ⏳ |
|
||||
| P0-6 | Sticky bar impact (indirect — aggregate checkout_started volume) | TBD | TBD | TBD | ⏳ |
|
||||
| P0-7/P0-8 | Trust copy impact (indirect — subscription_page → checkout_started) | TBD% | TBD% | TBDpp | ⏳ |
|
||||
| P0-9 | Social proof impression/engagement volume | TBD | TBD | TBD | ⏳ |
|
||||
|
||||
### 7.3 Recommendation
|
||||
|
||||
> ⏳ **PENDING** — To be written after monitoring window closes.
|
||||
|
||||
---
|
||||
|
||||
## 8. Data Collection Checklist
|
||||
|
||||
### Manual PostHog Steps (Require Dashboard Access)
|
||||
|
||||
1. [ ] Log in to PostHog at https://eu.posthog.com (project 127747)
|
||||
2. [ ] Navigate to **Product analytics → Funnels**
|
||||
3. [ ] Create funnel: **P0 CRO — checkout_started → payment_initiated** (Section 3.1 queries)
|
||||
4. [ ] Set date range to May 6 → May 12, 2026 — record baseline in Table 3A
|
||||
5. [ ] Set date range to May 13, 2026 — record Day 1 data in Table 4A
|
||||
6. [ ] Repeat for each metric in Sections 3–6 daily through May 16
|
||||
7. [ ] Create cohort funnels for new vs returning visitors (Section 5)
|
||||
8. [ ] Verify `payment_success` event volume is non-zero (confirms FN-281 is live)
|
||||
9. [ ] Fill in Final Verdict section (Section 7) after 72 hours
|
||||
10. [ ] Archive this document with "FINAL" status when complete
|
||||
|
||||
### Automated Collection (Future Enhancement)
|
||||
|
||||
See `scripts/posthog-dashboards.sh` for the existing dashboard creation script. A dedicated funnel monitoring script with API-based data extraction and table population can be created as a follow-up task.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: PostHog API Query Examples
|
||||
|
||||
If `POSTHOG_API_KEY` is available in the environment, the following queries can be run via the PostHog API:
|
||||
|
||||
```bash
|
||||
# Base configuration
|
||||
BASE="https://eu.posthog.com/api/projects/127747"
|
||||
AUTH="Authorization: Bearer $POSTHOG_API_KEY"
|
||||
|
||||
# Query checkout_started → payment_initiated funnel
|
||||
curl -s -X POST "$BASE/insights/" \
|
||||
-H "$AUTH" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "P0 CRO — Checkout Funnel (Post-Deploy)",
|
||||
"query": {
|
||||
"kind": "InsightVizNode",
|
||||
"source": {
|
||||
"kind": "FunnelsQuery",
|
||||
"series": [
|
||||
{"kind":"EventsNode","event":"checkout_started","name":"Checkout"},
|
||||
{"kind":"EventsNode","event":"payment_initiated","name":"Payment"}
|
||||
],
|
||||
"dateRange": {"date_from": "2026-05-13"},
|
||||
"funnelsFilter": {"funnelWindowInterval": 1, "funnelWindowIntervalUnit": "hour"}
|
||||
}
|
||||
}
|
||||
}' | jq '.result'
|
||||
```
|
||||
|
||||
```bash
|
||||
# Query full end-to-end funnel
|
||||
curl -s -X POST "$BASE/insights/" \
|
||||
-H "$AUTH" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "P0 CRO — Full End-to-End Funnel",
|
||||
"query": {
|
||||
"kind": "InsightVizNode",
|
||||
"source": {
|
||||
"kind": "FunnelsQuery",
|
||||
"series": [
|
||||
{"kind":"EventsNode","event":"checkout_started","name":"Checkout"},
|
||||
{"kind":"EventsNode","event":"payment_initiated","name":"Initiated"},
|
||||
{"kind":"EventsNode","event":"payment_success","name":"Success"}
|
||||
],
|
||||
"dateRange": {"date_from": "2026-05-13"},
|
||||
"funnelsFilter": {"funnelWindowInterval": 24, "funnelWindowIntervalUnit": "hour"}
|
||||
}
|
||||
}
|
||||
}' | jq '.result'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Code References
|
||||
|
||||
### B.1 PostHog Client Configuration
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `apps/web/src/lib/posthog.ts` | Frontend PostHog client (lazy-loaded `posthog-js`) |
|
||||
| `apps/api/src/posthog/posthog.service.ts` | Backend PostHog client (`posthog-node`) |
|
||||
| `apps/api/src/posthog/posthog.module.ts` | NestJS module registration |
|
||||
|
||||
### B.2 Event Source Locations
|
||||
|
||||
| Event | File | Line |
|
||||
|-------|------|------|
|
||||
| `plan_selected` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 327 |
|
||||
| `checkout_started` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 345 |
|
||||
| `payment_initiated` (iyzico) | `apps/web/src/components/payment/payment-content.tsx` | 196 |
|
||||
| `payment_initiated` (eft) | `apps/web/src/components/payment/payment-content.tsx` | 202 |
|
||||
| `payment_success` (iyzico redirect) | `apps/web/src/components/payment/payment-content.tsx` | 73 |
|
||||
| `payment_failed` (iyzico redirect) | `apps/web/src/components/payment/payment-content.tsx` | 76 |
|
||||
| `payment_success` (eft upload) | `apps/web/src/components/payment/payment-content.tsx` | 135 |
|
||||
| `receipt_uploaded` | `apps/web/src/components/payment/payment-content.tsx` | 209 |
|
||||
| `downgrade_offer_shown` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 365 |
|
||||
| `downgrade_offer_accepted` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 265 |
|
||||
| `downgrade_offer_declined` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 843 |
|
||||
| `cancel_flow_viewed` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 892 |
|
||||
| `cancel_save_clicked` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 269 |
|
||||
| `trial_urgency_banner_viewed` | `apps/web/src/components/trial-urgency-banner.tsx` | 77 |
|
||||
| `trial_urgency_banner_cta_clicked` | `apps/web/src/components/trial-urgency-banner.tsx` | 97 |
|
||||
| `trial_urgency_banner_dismissed` | `apps/web/src/components/trial-urgency-banner.tsx` | 87 |
|
||||
| `yearly_toggle_clicked` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 1055, 1066 |
|
||||
| `social_proof_impression` (subscription) | `apps/web/src/routes/dashboard/subscription/index.tsx` | 457, 474 |
|
||||
| `social_proof_engaged` (subscription) | `apps/web/src/routes/dashboard/subscription/index.tsx` | 484, 488 |
|
||||
| `social_proof_impression` (landing) | `apps/web/src/routes/index.tsx` | 572, 589, 606 |
|
||||
| `social_proof_impression` (auth) | `apps/web/src/routes/_auth.tsx` | 23 |
|
||||
| `social_proof_engaged` (landing) | `apps/web/src/routes/index.tsx` | 616 |
|
||||
| `social_proof_engaged` (auth) | `apps/web/src/routes/_auth.tsx` | 33 |
|
||||
| `subscription_cancelled` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 913 |
|
||||
| `subscription_resumed` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 753 |
|
||||
| `trial_started` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 952 |
|
||||
| `$pageview` (all routes) | `apps/web/src/routes/__root.tsx` | 95 |
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: Related Tasks & Documents
|
||||
|
||||
| ID | Description | Status |
|
||||
|----|-------------|--------|
|
||||
| FN-298 | P0 subscription CRO fixes deployment | ✅ Deployed May 13 |
|
||||
| FN-281 | `payment_success` / `payment_failed` instrumentation | ✅ Complete (`d855b56`) |
|
||||
| FN-309 | QA post-deploy verification | 🔄 In parallel |
|
||||
| FN-320 | Post-deploy QA — all 10 P0 fixes verified PASS | ✅ Complete |
|
||||
| FN-321 | PostHog payment events verification | ✅ Complete |
|
||||
| FN-275 | P1 CRO funnel audit (pre-instrumentation) | ✅ Complete |
|
||||
| `docs/product/funnel-audit-p1-cro-2026-05.md` | P1 CRO audit document | Reference |
|
||||
| `docs/product/cpo-brief-payment-events.md` | Payment events CPO brief | Reference |
|
||||
| `scripts/posthog-dashboards.sh` | PostHog dashboard creation script | Reference |
|
||||
|
||||
---
|
||||
|
||||
> **Document status:** 🔴 MONITORING ACTIVE — data collection pending
|
||||
> **Next update due:** May 14, 2026 (24h post-deploy)
|
||||
> **Final verdict due:** May 16, 2026 (72h post-deploy)
|
||||
Reference in New Issue
Block a user