# DigitalOcean App Platform

Route the ProductLense script and events to a small service component on DigitalOcean App Platform, which forwards them with the visitor's real IP address.

App Platform routes paths only to components of your own app, not to another domain. If your app already has a service component (a Node, Python, PHP or Go server), add the routes there with the guide for your framework: [Next.js](/docs/proxy/nextjs), [Flask](/docs/proxy/flask), [FastAPI](/docs/proxy/fastapi), [Laravel](/docs/proxy/laravel) or [PHP](/docs/proxy/php). For an app made only of a static site, add the small service below.

## A proxy service

App Platform puts the visitor's address in the `do-connecting-ip` header.

```js title="productlense-proxy/server.js"
import http from 'node:http';

const KEY = process.env.PL_API_KEY;
let script = { body: '', at: 0 };

http
  .createServer(async (req, res) => {
    try {
      if (req.method === 'GET' && req.url === '/js/a.js') {
        if (Date.now() - script.at > 3_600_000) {
          const r = await fetch('https://api.productlense.app/a.js');
          if (r.ok) script = { body: await r.text(), at: Date.now() };
        }
        if (!script.body) return res.writeHead(502).end();
        res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'public, max-age=86400' });
        return res.end(script.body);
      }
      if (req.method === 'POST' && req.url === '/m/api/e' && KEY) {
        let text = '';
        for await (const chunk of req) {
          text += chunk;
          if (text.length > 8192) return res.writeHead(413).end();
        }
        const body = JSON.parse(text);
        const extra = { ip: req.headers['do-connecting-ip'] ?? '', ua: req.headers['user-agent'] ?? '' };
        const out = body.b ? { ...body, b: body.b.map((e) => ({ ...e, ...extra })) } : { ...body, ...extra };
        const r = await fetch('https://api.productlense.app/api/v1/events', {
          method: 'POST',
          headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
          body: JSON.stringify(out),
        });
        return res.writeHead(r.status).end();
      }
      res.writeHead(404).end();
    } catch {
      res.writeHead(400).end();
    }
  })
  .listen(process.env.PORT || 8080);
```

```yaml title=".do/app.yaml (excerpt)"
services:
  - name: productlense-proxy
    source_dir: productlense-proxy
    run_command: node server.js
    http_port: 8080
    instance_size_slug: apps-s-1vcpu-0.5gb
    envs:
      - key: PL_API_KEY
        type: SECRET
        scope: RUN_TIME
ingress:
  rules:
    - match: { path: { prefix: /js/a.js } }
      component: { name: productlense-proxy, preserve_path_prefix: true }
    - match: { path: { prefix: /m/api/e } }
      component: { name: productlense-proxy, preserve_path_prefix: true }
    - match: { path: { prefix: / } }
      component: { name: web }
```

`preserve_path_prefix: true` passes the full path to the service, which the routes above expect. Replace `web` with the name of your existing component. `PL_API_KEY` is optional: without it the service serves only the script.

## Use it

```html tab="Script only (any plan)"
<script defer data-site="SITE_ID" data-api="https://api.productlense.app" src="/js/a.js"></script>
```

```html tab="Script and events (Scale)"
<script defer data-site="SITE_ID" data-api="https://example.com/m" src="/js/a.js"></script>
```

## Check it works

1. After the deploy finishes, open `https://example.com/js/a.js`: it returns JavaScript.
2. Load a page with the developer tools open. The Network tab shows a `POST` to `api/e` with status `204`.
3. Open the Live page in the ProductLense dashboard. Your visit appears within a few seconds.
