Next.js Integration Guide#

This guide covers how to integrate Pulsora Analytics with Next.js applications, supporting both the App Router (Next.js 13+) and Pages Router.

Installation#

First, install the required packages:

npm install @pulsora/core @pulsora/react

App Router Setup (Next.js 13+)#

1. Create Analytics Provider#

Create a client component for analytics:

// app/components/analytics-provider.tsx
'use client';

import { PulsoraProvider } from '@pulsora/react';
import { usePathname } from 'next/navigation';
import { usePageview } from '@pulsora/react';
import { useEffect } from 'react';

function PageviewTracker() {
  const pathname = usePathname();

  // Track pageviews on route change
  usePageview({ trigger: pathname });

  return null;
}

export function AnalyticsProvider({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <PulsoraProvider
      config={{
        apiToken: process.env.NEXT_PUBLIC_PULSORA_TOKEN!,
        debug: process.env.NODE_ENV === 'development'
      }}
    >
      <PageviewTracker />
      {children}
    </PulsoraProvider>
  );
}

2. Add to Root Layout#

Wrap your app with the analytics provider:

// app/layout.tsx
import { AnalyticsProvider } from '@/components/analytics-provider';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <AnalyticsProvider>
          {children}
        </AnalyticsProvider>
      </body>
    </html>
  );
}

3. Environment Variables#

Add your Pulsora API token to .env.local:

NEXT_PUBLIC_PULSORA_TOKEN=your-api-token-here

Pages Router Setup#

1. Custom App Component#

For Pages Router, modify _app.tsx:

// pages/_app.tsx
import { PulsoraProvider, usePageview } from '@pulsora/react';
import { useRouter } from 'next/router';
import type { AppProps } from 'next/app';

function PageviewTracker() {
  const router = useRouter();

  // Track pageviews on route change
  usePageview({ trigger: router.asPath });

  return null;
}

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <PulsoraProvider
      config={{
        apiToken: process.env.NEXT_PUBLIC_PULSORA_TOKEN!,
        debug: process.env.NODE_ENV === 'development'
      }}
    >
      <PageviewTracker />
      <Component {...pageProps} />
    </PulsoraProvider>
  );
}

Tracking Events#

Using Hooks#

Track custom events in your components:

// components/signup-form.tsx
import { useEvent } from '@pulsora/react';

export function SignupForm() {
  const trackEvent = useEvent();

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();

    // Track form submission
    trackEvent('signup_form_submit', {
      location: 'header',
      variant: 'modal'
    });

    // Your signup logic...
  };

  const handleFieldFocus = (fieldName: string) => {
    trackEvent('form_field_focus', {
      form: 'signup',
      field: fieldName
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        onFocus={() => handleFieldFocus('email')}
        placeholder="Email"
      />
      <button type="submit">Sign Up</button>
    </form>
  );
}

Server Components#

For server components, create a client wrapper:

// components/track-event-button.tsx
'use client';

import { useEvent } from '@pulsora/react';

export function TrackEventButton({
  eventName,
  eventData,
  children,
  ...props
}: {
  eventName: string;
  eventData?: Record<string, any>;
  children: React.ReactNode;
  [key: string]: any;
}) {
  const trackEvent = useEvent();

  return (
    <button
      {...props}
      onClick={(e) => {
        trackEvent(eventName, eventData);
        props.onClick?.(e);
      }}
    >
      {children}
    </button>
  );
}

Use in server components:

// app/products/page.tsx
import { TrackEventButton } from '@/components/track-event-button';

export default function ProductsPage() {
  return (
    <div>
      <h1>Our Products</h1>
      <TrackEventButton
        eventName="product_view"
        eventData={{ product: 'premium-plan' }}
        className="btn-primary"
      >
        View Premium Plan
      </TrackEventButton>
    </div>
  );
}

Revenue Tracking#

Checkout Integration#

Pass tracking data to your payment processor:

// app/api/checkout/route.ts
import { NextRequest } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: NextRequest) {
  const { priceId, visitor_fingerprint, session_id } = await request.json();

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        price: priceId,
        quantity: 1,
      },
    ],
    mode: 'payment',
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/cancel`,
    metadata: {
      visitor_fingerprint,
      session_id,
    },
  });

  return Response.json({ sessionId: session.id });
}

Client-Side Checkout#

// components/checkout-button.tsx
'use client';

import { usePulsora } from '@pulsora/react';
import { loadStripe } from '@stripe/stripe-js';

export function CheckoutButton({ priceId }: { priceId: string }) {
  const pulsora = usePulsora();

  const handleCheckout = async () => {
    // Get tracking data
    const fingerprint = await pulsora.getVisitorFingerprint();
    const sessionId = pulsora.getSessionId();

    // Create checkout session
    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        priceId,
        visitor_fingerprint: fingerprint,
        session_id: sessionId
      })
    });

    const { sessionId: stripeSessionId } = await response.json();

    // Redirect to Stripe
    const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!);
    await stripe!.redirectToCheckout({ sessionId: stripeSessionId });
  };

  return (
    <button onClick={handleCheckout}>
      Subscribe Now
    </button>
  );
}

API Routes#

Track Events Server-Side#

For server-side event tracking:

// app/api/track/route.ts
import { Pulsora } from '@pulsora/core';
import { NextRequest } from 'next/server';

// Initialize server-side tracker
const pulsora = new Pulsora();
pulsora.init({
  apiToken: process.env.PULSORA_SERVER_TOKEN!,
});

export async function POST(request: NextRequest) {
  const { eventName, eventData, visitorFingerprint, sessionId } =
    await request.json();

  // Track event with visitor context
  await pulsora.event(eventName, {
    ...eventData,
    visitor_fingerprint: visitorFingerprint,
    session_id: sessionId,
  });

  return Response.json({ success: true });
}

Middleware Integration#

Track server-side events using Next.js middleware:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Track API usage
  if (request.nextUrl.pathname.startsWith('/api/')) {
    // Log API access (implement your tracking logic)
    console.log('API accessed:', {
      path: request.nextUrl.pathname,
      method: request.method,
      timestamp: new Date().toISOString(),
    });
  }

  return response;
}

export const config = {
  matcher: '/api/:path*',
};

Static Site Generation (SSG)#

For static pages, track pageviews client-side only:

// pages/blog/[slug].tsx (Pages Router)
import { usePageview } from '@pulsora/react';

export default function BlogPost({ post }: { post: Post }) {
  // Track pageview on client-side mount
  usePageview();

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

export async function getStaticProps({ params }) {
  // Fetch post data
  const post = await getPost(params.slug);

  return {
    props: { post },
    revalidate: 3600 // ISR: revalidate every hour
  };
}

Performance Optimization#

Dynamic Import#

Load Pulsora only in the browser:

// app/components/dynamic-analytics.tsx
import dynamic from 'next/dynamic';

const AnalyticsProvider = dynamic(
  () => import('./analytics-provider').then(mod => mod.AnalyticsProvider),
  { ssr: false }
);

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <AnalyticsProvider>
      {children}
    </AnalyticsProvider>
  );
}

Conditional Loading#

Load analytics based on user consent:

// app/components/conditional-analytics.tsx
'use client';

import { useState, useEffect } from 'react';
import { PulsoraProvider } from '@pulsora/react';

export function ConditionalAnalytics({
  children
}: {
  children: React.ReactNode
}) {
  const [hasConsent, setHasConsent] = useState(false);

  useEffect(() => {
    // Check for user consent
    const consent = localStorage.getItem('analytics-consent');
    setHasConsent(consent === 'true');
  }, []);

  if (!hasConsent) {
    return <>{children}</>;
  }

  return (
    <PulsoraProvider
      config={{
        apiToken: process.env.NEXT_PUBLIC_PULSORA_TOKEN!
      }}
    >
      {children}
    </PulsoraProvider>
  );
}

Debugging#

Enable Debug Mode#

Set debug mode in development:

// app/components/analytics-provider.tsx
<PulsoraProvider
  config={{
    apiToken: process.env.NEXT_PUBLIC_PULSORA_TOKEN!,
    debug: process.env.NODE_ENV === 'development',
    endpoint: process.env.NEXT_PUBLIC_PULSORA_ENDPOINT // Optional custom endpoint
  }}
>

Verify Installation#

Check the browser console for debug logs:

[Pulsora] Initialized with token: pub_***
[Pulsora] Pageview tracked: /products
[Pulsora] Event tracked: button_click { location: 'header' }

Common Issues#

Hydration Mismatch#

If you encounter hydration errors, ensure analytics code runs only on the client:

// Use dynamic import with ssr: false
const Analytics = dynamic(() => import('./analytics'), { ssr: false });

Missing Environment Variables#

Ensure your .env.local file contains:

NEXT_PUBLIC_PULSORA_TOKEN=your-public-api-token

For server-side tracking, also add:

PULSORA_SERVER_TOKEN=your-server-api-token

Route Changes Not Tracked#

Ensure you're using the correct router hook:

  • App Router: usePathname() from next/navigation
  • Pages Router: useRouter() from next/router

Next Steps#