PostHog مع Next.js: تحليلات المنتج، أعلام الميزات، وإعادة عرض الجلسات (دليل 2026)

أصبحت PostHog بهدوء أكثر منصات المنتج اكتمالاً لفِرق SaaS في عام 2026. ما كان يتطلب Mixpanel للتحليلات، وLaunchDarkly لأعلام الميزات، وFullStory لإعادة تشغيل الجلسات، وOptimizely لاختبارات A/B، وSentry لرصد الأخطاء، أصبح كله ضمن منصة واحدة مفتوحة المصدر، مع باقة مجانية سخية وإمكانية الاستضافة الذاتية.
في هذا الدرس ستوصِّل PostHog ضمن مشروع Next.js 15 يستخدم App Router من البداية إلى النهاية. في النهاية ستقوم بتتبع الأحداث، وإخفاء الميزات خلف أعلام، وتسجيل جلسات المستخدمين، وإجراء تجربة، والتقاط الاستثناءات — باستخدام مزوّد posthog-js/react الحديث وأنماط SSR والعميل الصحيحة.
المتطلبات الأساسية
قبل أن تبدأ، تأكد من توفر:
- Node.js 20 أو أحدث
- معرفة أساسية بـ Next.js App Router ومكونات React الخادمة
- حساب على PostHog Cloud (الباقة المجانية كافية) — التسجيل عبر posthog.com، أو نسخة ذاتية الاستضافة
- محرر أكواد (يُفضَّل VS Code)
ينبغي أن تكون مرتاحاً مع متغيرات البيئة والفرق بين مكونات الخادم ومكونات العميل في Next.js.
ما الذي ستبنيه
ستبني لوحة تحكم بأسلوب SaaS تقوم بـ:
- التقاط مشاهدات الصفحات والأحداث المخصصة على الجانبين العميل والخادم
- التعرف على المستخدمين المسجَّلين وإرفاق خصائص بهم
- عرض واجهة "لوحة تحكم جديدة" بشكل شرطي خلف feature flag
- إجراء تجربة A/B على زر CTA في صفحة التسعير
- تسجيل جلسات المستخدمين مع إخفاء حقول الإدخال
- الإبلاغ عن الاستثناءات غير المعالَجة إلى Error Tracking في PostHog
ستبدو بنية المشروع النهائية كالتالي:
app/
layout.tsx
providers.tsx
page.tsx
pricing/page.tsx
api/track/route.ts
lib/
posthog-server.ts
.env.local
الخطوة 1: إنشاء المشروع وتثبيت التبعيات
ابدأ بمشروع Next.js 15 نظيف (تجاوز هذه الخطوة إن كان لديك مشروع جاهز):
npx create-next-app@latest posthog-demo --typescript --app --tailwind --eslint
cd posthog-demoثبّت حزم PostHog. تحتاج إلى حزمتين: posthog-js للمتصفح، وposthog-node للالتقاط من جهة الخادم.
npm install posthog-js posthog-nodeفي 2026، التكامل الرسمي مع Next.js موثَّق حول مزوّد posthog-js/react، الذي يقدم واجهة قائمة على الـ hooks ويُغني عن استدعاءات الالتقاط اليدوية داخل useEffect.
الخطوة 2: ضبط متغيرات البيئة
أنشئ ملف .env.local في جذر المشروع:
NEXT_PUBLIC_POSTHOG_KEY=phc_your_project_api_key
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
POSTHOG_PERSONAL_API_KEY=phx_your_personal_api_keyبعض التفاصيل المهمة:
- المتغيرات بادئتها
NEXT_PUBLIC_*مكشوفة للمتصفح — وهذا مقصود بالنسبة لمفتاح المشروع، فهو رمز عام مخصص لالتقاط أحداث العميل. - استخدم
eu.i.posthog.comإذا كان مشروع PostHog في أوروبا، وإلا فاستخدمus.i.posthog.com. اختيار المنطقة الخاطئة يُسقط الأحداث بصمت. - المفتاح الشخصي يُستخدم فقط لمهام الإدارة من جهة الخادم (مثل قراءة تعريفات الأعلام)؛ ولا يجب أبداً أن يصل إلى المتصفح.
الخطوة 3: إعداد مزوّد المتصفح
أنشئ app/providers.tsx. هذا الملف هو مكوّن عميل يهيّئ PostHog مرة واحدة ويُتاحه لباقي شجرتك عبر PostHogProvider.
// app/providers.tsx
"use client";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
import { useEffect } from "react";
export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
capture_pageview: false, // نلتقطه يدوياً مع App Router
capture_pageleave: true,
person_profiles: "identified_only",
session_recording: {
maskAllInputs: true,
},
});
}, []);
return <PHProvider client={posthog}>{children}</PHProvider>;
}خياران يستحقان الفهم:
capture_pageview: false— Next.js App Router يقوم بملاحة لينة، فلا يُطلِق SDK مشاهدة الصفحة الآلية إلا مرة واحدة عند التحميل الكامل. سنلتقط مشاهدات الصفحات بأنفسنا عند كل تغيير مسار.person_profiles: "identified_only"— الزوّار المجهولون لا يُنشَأ لهم ملف شخصي، مما يبقي فاتورة MAU أدنى ما يمكن حتى تتعرّف فعلياً على مستخدم.
الخطوة 4: تتبع مشاهدات الصفحات عند تغير المسار
أضف مكوّن عميل صغير يستمع إلى ملاحة App Router ويُطلِق حدث $pageview عند كل تغيير في المسار.
// app/posthog-pageview.tsx
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
export function PostHogPageview() {
const pathname = usePathname();
const searchParams = useSearchParams();
const posthog = usePostHog();
useEffect(() => {
if (!pathname || !posthog) return;
let url = window.origin + pathname;
const search = searchParams?.toString();
if (search) url += `?${search}`;
posthog.capture("$pageview", { $current_url: url });
}, [pathname, searchParams, posthog]);
return null;
}والآن وصِّل الكل في الـ layout الجذري:
// app/layout.tsx
import { Suspense } from "react";
import { PostHogProvider } from "./providers";
import { PostHogPageview } from "./posthog-pageview";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ar" dir="rtl">
<body>
<PostHogProvider>
<Suspense fallback={null}>
<PostHogPageview />
</Suspense>
{children}
</PostHogProvider>
</body>
</html>
);
}تغليف Suspense مطلوب لأن useSearchParams يطلق bailout للـ CSR — بدونه، تنتقل الصفحة بكاملها إلى الرندرة من جانب العميل.
الخطوة 5: التقاط أحداث مخصصة من المكونات
يمكنك الآن التقاط الأحداث من أي مكوّن عميل عبر hook الاسم usePostHog:
// app/page.tsx
"use client";
import { usePostHog } from "posthog-js/react";
export default function Home() {
const posthog = usePostHog();
return (
<main className="p-12">
<h1 className="text-3xl font-bold">مرحباً</h1>
<button
className="mt-6 rounded bg-black px-4 py-2 text-white"
onClick={() =>
posthog.capture("cta_clicked", {
location: "homepage_hero",
variant: "primary",
})
}
>
ابدأ الآن
</button>
</main>
);
}قاعدتان أساسيتان لتصميم الأحداث:
- أسماء الأحداث تستخدم
snake_caseوفعلاً في صيغة الماضي:signup_completed,invoice_downloaded,chat_message_sent. تجنب الأسماء العامة مثلclickأوsubmit. - الخصائص مسطّحة، مُحدَّدة الأنواع، ومحدودة. لا تُلقِ بكائنات كاملة؛ اختر من 3 إلى 8 حقول ستقوم فعلاً بالتصفية بناءً عليها لاحقاً.
الخطوة 6: التعرف على المستخدمين المسجَّلين
الأحداث المجهولة مفيدة للقمع، لكن القيمة الحقيقية تأتي من ربط الأحداث بمستخدم معروف. استدعِ identify مرة واحدة بعد تسجيل الدخول:
"use client";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
type User = { id: string; email: string; plan: "free" | "pro" | "team" };
export function IdentifyUser({ user }: { user: User | null }) {
const posthog = usePostHog();
useEffect(() => {
if (!user || !posthog) return;
posthog.identify(user.id, {
email: user.email,
plan: user.plan,
});
posthog.group("plan", user.plan);
}, [user, posthog]);
return null;
}عندما يُسجِّل المستخدم خروجه، استدعِ posthog.reset() لفصل الـ distinct ID وبدء جلسة مجهولة جديدة.
الخطوة 7: التقاط أحداث من جانب الخادم
بعض الأحداث لا ينبغي أن تعيش أبداً في المتصفح — نجاح الدفع، استلام webhook، انتهاء استدلال الذكاء الاصطناعي. لهذه استخدم posthog-node.
أنشئ lib/posthog-server.ts:
// lib/posthog-server.ts
import { PostHog } from "posthog-node";
let client: PostHog | null = null;
export function getPostHogServer() {
if (!client) {
client = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
flushAt: 1,
flushInterval: 0,
});
}
return client;
}إعدادات الـ flush الصارمة مهمة في بيئات serverless: مع التجميع الافتراضي، ستُفقد الأحداث الملتقَطة داخل دالة Vercel عند تجميد lambda قبل الإفراغ.
استخدمه في route handler:
// app/api/track/route.ts
import { NextResponse } from "next/server";
import { getPostHogServer } from "@/lib/posthog-server";
export async function POST(req: Request) {
const { userId, plan } = await req.json();
const posthog = getPostHogServer();
posthog.capture({
distinctId: userId,
event: "subscription_started",
properties: { plan, source: "stripe_webhook" },
});
await posthog.shutdown();
return NextResponse.json({ ok: true });
}استخدم دائماً await posthog.shutdown() (أو على الأقل posthog.flush()) قبل الخروج من handler serverless. وإلا فإن الحدث يبقى في طابور بالذاكرة سيختفي مع lambda.
الخطوة 8: إطلاق ميزة خلف feature flag
أنشئ feature flag منطقياً في واجهة PostHog باسم new-dashboard. اضبط التفعيل على "100 بالمئة من المستخدمين بخاصية plan = pro" لكي يراه فقط العملاء المدفوعون.
من جانب العميل، يقرأ hook الاسم useFeatureFlagEnabled العَلم المخزَّن مؤقتاً:
// app/dashboard/page.tsx
"use client";
import { useFeatureFlagEnabled } from "posthog-js/react";
export default function Dashboard() {
const newDashboard = useFeatureFlagEnabled("new-dashboard");
if (newDashboard) {
return <NewDashboard />;
}
return <LegacyDashboard />;
}هناك مشكلة UX دقيقة هنا: في أول رندر يكون العَلم بقيمة undefined، ثم ينقلب إلى true أو false، مما يسبب وميضاً ظاهراً. أصلحها بتقييم العَلم على الخادم وتمرير القيمة الناتجة إلى الأسفل.
// app/dashboard/page.tsx
import { getPostHogServer } from "@/lib/posthog-server";
import { cookies } from "next/headers";
export default async function DashboardPage() {
const posthog = getPostHogServer();
const distinctId = (await cookies()).get("ph_distinct_id")?.value ?? "anonymous";
const newDashboard = await posthog.isFeatureEnabled("new-dashboard", distinctId);
return newDashboard ? <NewDashboard /> : <LegacyDashboard />;
}لكي يكون التقييم على الخادم منطقياً، يجب أن يكون الـ distinct ID ثابتاً عبر الطلبات. أبسط طريقة هي قراءته من كوكي يُوضع بعد identify، أو استخدام معرف المستخدم من مزوّد المصادقة.
الخطوة 9: إجراء تجربة A/B
الأعلام متعددة المتغيرات هي محرك التجارب. في واجهة PostHog، أنشئ تجربة باسم pricing_cta_test بمتغيرين: control (نص "ابدأ تجربة مجانية") وbold (نص "انطلق مجاناً، بدون بطاقة بنكية").
// app/pricing/page.tsx
"use client";
import { useFeatureFlagVariantKey, usePostHog } from "posthog-js/react";
export default function PricingPage() {
const variant = useFeatureFlagVariantKey("pricing_cta_test");
const posthog = usePostHog();
const ctaText = variant === "bold"
? "انطلق مجاناً، بدون بطاقة بنكية"
: "ابدأ تجربة مجانية";
return (
<button
onClick={() => {
posthog.capture("pricing_cta_clicked", { variant });
}}
>
{ctaText}
</button>
);
}ضع دائماً قيمة variant كخاصية في حدث التحويل. عندها يستطيع PostHog حساب الـ lift والدلالة الإحصائية والفترات الموثوقة آلياً — وستحصل على البيانات الخام في عرض الـ warehouse إذا أردت تقطيعها بنفسك.
الخطوة 10: تفعيل إعادة عرض الجلسات مع إخفاء المدخلات
تم تفعيل إعادة عرض الجلسة في الخطوة 3 عبر maskAllInputs: true. هذا يغطي الأساس، لكن ينبغي أن تكون أكثر دقة فيما يتم تسجيله. أضف فئات CSS على العناصر التي لا تريد أبداً التقاطها:
<input
type="text"
className="ph-no-capture"
placeholder="الرقم الضريبي"
/>
<div className="ph-mask">
<p>بنود تعاقدية حساسة هنا.</p>
</div>ثلاث فئات يتعرّف عليها المسجِّل:
ph-no-capture— يُعامَل العنصر كأنه غير موجود في snapshot الـ DOMph-mask— يُستبدَل العنصر بمستطيل أسود بنفس الحجمph-mask-text— يُستبدَل النص بنجوم مع الحفاظ على التخطيط
اجمع هذه الفئات مع maskAllInputs: true لإطلاق إعادة العرض بأمان، حتى في القطاعات المنظَّمة. وللامتثال الكامل لـ GDPR، اشترط أيضاً تسجيل الجلسة على موافقة المستخدم قبل استدعاء posthog.startSessionRecording().
الخطوة 11: التقاط الاستثناءات
منتج Error Tracking من PostHog يتصل بنفس الـ SDK. فعِّله بإتاحة الالتقاط التلقائي للاستثناءات في init:
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
// ... الإعدادات الموجودة
capture_exceptions: true,
});يمكنك أيضاً الإبلاغ عن الأخطاء يدوياً مع stack trace من error boundary في Next.js:
// app/error.tsx
"use client";
import { useEffect } from "react";
import { usePostHog } from "posthog-js/react";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
const posthog = usePostHog();
useEffect(() => {
posthog.captureException(error, {
digest: (error as Error & { digest?: string }).digest,
});
}, [error, posthog]);
return (
<div>
<h2>حدث خطأ ما</h2>
<button onClick={reset}>أعد المحاولة</button>
</div>
);
}هذا يملأ علامة Errors في PostHog بـ stack trace، وبفتات الخبز من الأحداث السابقة، ورابط إلى تسجيل الجلسة الذي وقع فيه الخطأ — وهذه عادةً الميزة الفارقة التي تُبرر الانتقال من Sentry.
الخطوة 12: تجاوز موانع الإعلانات عبر reverse proxy
في الإنتاج، نحو 30 بالمئة من الزوار يستخدمون مانع إعلانات يحجب *.posthog.com. الحل هو reverse proxy عبر نطاقك الخاص. في Next.js 15 أضف هذا إلى next.config.ts:
const nextConfig = {
async rewrites() {
return [
{
source: "/ingest/static/:path*",
destination: "https://eu-assets.i.posthog.com/static/:path*",
},
{
source: "/ingest/:path*",
destination: "https://eu.i.posthog.com/:path*",
},
];
},
skipTrailingSlashRedirect: true,
};ثم غيِّر إعداد SDK لاستخدام أصلك الخاص:
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: "/ingest",
ui_host: "https://eu.posthog.com",
});استرجاع الأحداث من حركة المرور المحجوبة يرفع عادةً بيانات التحويل بنسبة 10 إلى 30 بالمئة — وهذا مردود ممتاز مقابل خمس دقائق من الإعداد.
اختبار الإعداد
تحقق من عمل كل شيء قبل إعلان النصر:
- افتح علامة Activity في PostHog وحمّل صفحتك الرئيسية في نافذة تخفّي. ينبغي أن ترى حدث
$pageviewخلال ثوانٍ. - انقر زر "ابدأ الآن" وتأكد أن
cta_clickedيصل بخصائصlocationوvariant. - سجّل دخولك إلى التطبيق وتأكد أن الحدث التالي يحمل البريد الإلكتروني والباقة كخصائص شخص.
- بدّل العَلم
new-dashboardفي الواجهة وأعد تحميل صفحة لوحة التحكم — ينبغي أن يتغير المكوّن المعروض دون إعادة نشر. - افتح Session Replay وتأكد من إخفاء حقول الإدخال.
- أطلق استثناءً غير معالَج وتحقق من ظهوره في Errors مع stack trace.
إن تأخّرت الأحداث عن الظهور أكثر من دقيقة، فالسبب الأشيع هو عدم تطابق المنطقة في NEXT_PUBLIC_POSTHOG_HOST.
استكشاف الأخطاء وإصلاحها
الأحداث تُطلَق محلياً لكن ليس في الإنتاج. غالباً مشكلة Content Security Policy أو مانع إعلانات. استخدم reverse proxy من الخطوة 12 وافحص علامة Network للطلبات /ingest المحجوبة.
العَلم يعود undefined إلى ما لا نهاية. SDK لم يكمل التشغيل بعد. إما انتظر posthog.onFeatureFlags() أو قم بالتقييم من الخادم كما في الخطوة 8.
أحداث الخادم لا تصل أبداً. نسيت await posthog.shutdown() في نهاية route handler. serverless يجمّد الدالة، فيُفقد الباتش في الذاكرة.
فاتورة MAU انفجرت بعد الإطلاق. نسيت person_profiles: "identified_only" فأنشأ PostHog ملفاً لكل زائر مجهول. غيّر الإعداد وتواصل مع الدعم لإعادة ضبط عدّاد الملفات.
الخطوات التالية
- وجّه أحداث PostHog إلى مستودع البيانات لديك عبر وجهات BigQuery أو Snowflake أو Postgres
- اجمع مع رسائل Resend الإلكترونية لإطلاق رسائل من مجموعات PostHog
- أضف Better Auth وعرّف المستخدمين فور تسجيل الدخول
- اعتمد مراقبة OpenTelemetry لتتبّع شامل في الواجهة الخلفية
الخاتمة
تحوّل PostHog خمسة اشتراكات SaaS منفصلة إلى منصة واحدة مفتوحة المصدر يمكنك استضافتها ذاتياً عند الحاجة. عبر الخطوات السابقة لديك تحليلات وأعلام ميزات وتجارب وإعادة عرض جلسات وتتبّع أخطاء، تعمل في تطبيق Next.js 15 App Router — مع رندرة سليمة على الخادم، ومرونة أمام موانع الإعلانات، وحساسية لخصوصية المستخدم.
الفائدة الحقيقية تظهر في الأسابيع التالية: كل قرار منتج يصير له رقم خلفه، وكل ميزة تنطلق خلف عَلم يمكنك تبديله في ثوان، وكل بلاغ خطأ يأتي معه تسجيل جلسة. هذا التحوّل في طريقة العمل يستحق نصف يوم الإعداد.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء مشروع SaaS متكامل باستخدام Next.js 15 و Stripe و Auth.js v5
تعلم كيفية بناء تطبيق SaaS جاهز للإنتاج باستخدام Next.js 15 مع نظام اشتراكات Stripe ومصادقة Auth.js v5. يغطي هذا الدليل التفصيلي إعداد المشروع وتسجيل الدخول عبر OAuth وخطط الأسعار ومعالجة Webhooks والمسارات المحمية.

بناء وكيل ذكاء اصطناعي مستقل باستخدام Agentic RAG و Next.js
تعلم كيف تبني وكيل ذكاء اصطناعي يقرر بشكل مستقل متى وكيف يسترجع المعلومات من قواعد البيانات المتجهية. دليل عملي شامل باستخدام Vercel AI SDK و Next.js مع أمثلة قابلة للتنفيذ.

بناء محرك بحث دلالي بالذكاء الاصطناعي مع Next.js 15 و OpenAI و Pinecone
تعلّم كيف تبني محرك بحث دلالي متقدّم باستخدام Next.js 15 و OpenAI Embeddings و قاعدة بيانات Pinecone المتجهية. يغطي هذا الدليل الشامل من الإعداد إلى النشر مع Server Actions وواجهة بحث تفاعلية.