AddonPulse
Proxy Guide

Astro Proxy Setup

Configure Astro middleware to proxy AddonPulse tracking requests

Astro's middleware feature makes it simple to proxy AddonPulse tracking. This guide shows how to set up server-side request proxying in your Astro application.

Overview

Astro middleware allows you to intercept and modify requests before they're handled, making it perfect for creating a transparent analytics proxy.

What you'll achieve:

  • Proxy AddonPulse endpoints through your Astro site
  • Forward necessary headers for accurate tracking
  • Simple configuration in astro.config.mjs
  • Support all AddonPulse features

Prerequisites

  • Astro 2.0 or later (with SSR or hybrid rendering enabled)
  • Your AddonPulse instance URL https://app.addonpulse.com
  • Your AddonPulse site ID

This guide requires SSR (Server-Side Rendering) or hybrid mode. Static sites cannot proxy requests server-side. For static sites on platforms like Vercel or Netlify, use their platform-specific proxy configurations.

Implementation

Enable SSR in Astro

Update astro.config.mjs to enable SSR:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server', // or 'hybrid'
  adapter: node({
    mode: 'standalone',
  }),
});

Install the Node adapter if not already installed:

npm install @astrojs/node

Configure Environment Variables

Create or update .env:

# .env
ADDONPULSE_HOST=https://app.addonpulse.com

Create Middleware

Create src/middleware.ts (or .js for JavaScript):

// src/middleware.ts
import type { MiddlewareHandler } from 'astro';

const ADDONPULSE_HOST = import.meta.env.ADDONPULSE_HOST || 'https://app.addonpulse.com';

export const onRequest: MiddlewareHandler = async (context, next) => {
  const { request } = context;
  const url = new URL(request.url);

  // Check if this is an analytics request
  if (url.pathname.startsWith('/analytics/')) {
    return proxyToAddonpulse(request, url.pathname);
  }

  // Continue with normal request handling
  return next();
};

async function proxyToAddonpulse(request: Request, pathname: string): Promise<Response> {
  try {
    // Map paths: /analytics/* → /api/*
    let addonpulsePath = pathname.replace('/analytics/', '/api/');

    const addonpulseUrl = `${ADDONPULSE_HOST}${addonpulsePath}`;

    // Get client IP from headers
    const forwardedFor = request.headers.get('x-forwarded-for');
    const clientIp = forwardedFor ? forwardedFor.split(',')[0].trim() : '127.0.0.1';

    // Forward request
    const proxyHeaders = new Headers(request.headers);
    proxyHeaders.set('X-Real-IP', clientIp);
    proxyHeaders.set('X-Forwarded-For', clientIp);

    const response = await fetch(addonpulseUrl, {
      method: request.method,
      headers: proxyHeaders,
      body: request.method !== 'GET' ? await request.text() : undefined,
    });

    // Return proxied response
    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: response.headers,
    });
  } catch (error) {
    console.error('AddonPulse proxy error:', error);
    return new Response('Analytics proxy error', { status: 500 });
  }
}

For JavaScript projects:

// src/middleware.js
const ADDONPULSE_HOST = import.meta.env.ADDONPULSE_HOST || 'https://app.addonpulse.com';

export const onRequest = async (context, next) => {
  const { request } = context;
  const url = new URL(request.url);

  if (url.pathname.startsWith('/analytics/')) {
    return proxyToAddonpulse(request, url.pathname);
  }

  return next();
};

async function proxyToAddonpulse(request, pathname) {
  try {
    // Map paths: /analytics/* → /api/*
    let addonpulsePath = pathname.replace('/analytics/', '/api/');

    const addonpulseUrl = `${ADDONPULSE_HOST}${addonpulsePath}`;
    const forwardedFor = request.headers.get('x-forwarded-for');
    const clientIp = forwardedFor ? forwardedFor.split(',')[0].trim() : '127.0.0.1';

    const proxyHeaders = new Headers(request.headers);
    proxyHeaders.set('X-Real-IP', clientIp);
    proxyHeaders.set('X-Forwarded-For', clientIp);

    const response = await fetch(addonpulseUrl, {
      method: request.method,
      headers: proxyHeaders,
      body: request.method !== 'GET' ? await request.text() : undefined,
    });

    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: response.headers,
    });
  } catch (error) {
    console.error('AddonPulse proxy error:', error);
    return new Response('Analytics proxy error', { status: 500 });
  }
}

Update Your Layout

Add the tracking script to your layout:

---
// src/layouts/Layout.astro
const { title } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>

    <!-- AddonPulse Analytics -->
    <script src="/analytics/script.js" async data-site-id="YOUR_SITE_ID"></script>
  </head>
  <body>
    <slot />
  </body>
</html>

Start Development Server

npm run dev

Verify the Setup

  1. Open your site in a browser with Developer Tools
  2. Check Network tab: Requests should go to /analytics/*
  3. Verify in AddonPulse dashboard: Data should appear

How It Works

Astro middleware intercepts requests before they're processed:

  1. Request to /analytics/script.js enters middleware
  2. Middleware proxies to https://app.addonpulse.com/api/script.js
  3. Client IP headers are preserved
  4. Response is returned to browser

Advanced Configuration

Caching with Astro

Add caching for scripts:

// src/middleware.ts
const cache = new Map<string, { response: Response; timestamp: number }>();
const CACHE_TTL = 3600000; // 1 hour in milliseconds

async function proxyToAddonpulse(request: Request, pathname: string): Promise<Response> {
  // Cache GET requests for scripts
  if (request.method === 'GET' && pathname.endsWith('.js')) {
    const cached = cache.get(pathname);

    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
      return cached.response.clone();
    }
  }

  // ... proxy logic

  // Cache the response
  if (request.method === 'GET' && pathname.endsWith('.js')) {
    cache.set(pathname, {
      response: response.clone(),
      timestamp: Date.now(),
    });
  }

  return response;
}

Conditional Proxying

Only proxy in production:

export const onRequest: MiddlewareHandler = async (context, next) => {
  const { request } = context;
  const url = new URL(request.url);

  // Only proxy in production
  if (import.meta.env.PROD && url.pathname.startsWith('/analytics/')) {
    return proxyToAddonpulse(request, url.pathname);
  }

  return next();
};

Error Logging

Add detailed error logging:

async function proxyToAddonpulse(request: Request, pathname: string): Promise<Response> {
  try {
    // ... proxy logic
  } catch (error) {
    const errorDetails = {
      message: error instanceof Error ? error.message : 'Unknown error',
      pathname,
      method: request.method,
      timestamp: new Date().toISOString(),
    };

    console.error('AddonPulse proxy error:', errorDetails);

    // Return error response
    return new Response(JSON.stringify({ error: 'Analytics proxy error' }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

Troubleshooting

Middleware not running

Problem: Requests bypass middleware.

Solution:

  1. Ensure SSR or hybrid mode is enabled in astro.config.mjs
  2. Verify middleware file is in src/middleware.ts (not in subdirectory)
  3. Check adapter is installed: npm install @astrojs/node

Static build issues

Problem: Build fails or proxy doesn't work in production.

Solution: Middleware requires server-side rendering. Ensure:

// astro.config.mjs
export default defineConfig({
  output: 'server', // Required for middleware
  adapter: node({
    mode: 'standalone',
  }),
});

Environment variables not loading

Problem: ADDONPULSE_HOST is undefined.

Solution:

  1. Check .env file is in project root
  2. Use import.meta.env.ADDONPULSE_HOST (not process.env)
  3. For production, set environment variables in your hosting platform

Deployment

Vercel

Deploy with Vercel adapter:

npm install @astrojs/vercel
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'server',
  adapter: vercel(),
});

Netlify

Deploy with Netlify adapter:

npm install @astrojs/netlify
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';

export default defineConfig({
  output: 'server',
  adapter: netlify(),
});

Node.js Server

For self-hosting:

npm run build
node ./dist/server/entry.mjs

On this page