# CountWatch recipe: Base44 backend functions

Counts each call of your app's backend functions as one hit to a webhook URL, with the function's group and the outcome (`ok` or `error`) as dimensions. Each group × outcome is one watch (a push watch: it updates at once; mind the plan's watch limit and group related functions).

**Base44 traps first:** editing a function file (in the sandbox, or with Base44's `edit_file`, `write_file` or `run_command`) does not deploy it. Deploy each function ("Save & Redeploy" in the editor, or `base44 functions deploy`), then **publish the app again**: the published app keeps the old functions until then. Secrets reach functions as `Deno.env`, not the sandbox shell.

## 1. The webhook URL

The person makes a webhook URL for the metric (for example `app_actions`) in the dashboard (Sources: https://magnum-staging.elirais.workers.dev/dashboard/sources) and adds it as the Base44 secret `COUNTWATCH_WEBHOOK_URL`. It is a secret: keep it out of chat (link previews are ignored, but anyone who has the link can send counts).

## 2. The wrapper

Save as `functions/shared/countwatch.ts` (or next to each function):

```ts
// countwatch.ts — CountWatch for Base44 backend functions (Deno). Wrap a function's handler and each call
// sends one hit to COUNTWATCH_WEBHOOK_URL with ?group=<group>&outcome=ok|error: ok on 2xx, error on 5xx or a thrown
// error; a 4xx (the caller's mistake) is not counted. It never changes the response, waits at most 1.5 s, and does
// nothing when the secret is not set.
type Handler = (req: Request) => Response | Promise<Response>;

async function hit(group: string, outcome: "ok" | "error"): Promise<void> {
  const link = Deno.env.get("COUNTWATCH_WEBHOOK_URL");
  if (!link) return;
  const url = new URL(link);
  url.searchParams.set("group", group);
  url.searchParams.set("outcome", outcome);
  try {
    await fetch(url, { method: "POST", signal: AbortSignal.timeout(1500) });
  } catch {
    // counting never breaks the function
  }
}

export function withCountWatch(group: string, handler: Handler): Handler {
  return async (req) => {
    let res: Response;
    try {
      res = await handler(req);
    } catch (err) {
      await hit(group, "error");
      throw err;
    }
    if (res.status >= 200 && res.status < 300) await hit(group, "ok");
    else if (res.status >= 500) await hit(group, "error");
    return res;
  };
}
```

Wrap each function's handler:

```ts
import { withCountWatch } from "../shared/countwatch.ts";

Deno.serve(withCountWatch("finalize", async (req) => {
  // … the function as before
  return Response.json({ ok: true });
}));
```

Do not wrap the function that receives CountWatch alerts: its calls would feed back into the counts.

## 3. Deploy and test

Redeploy every wrapped function, then publish the app. Call one function without signing in (a function that calls `base44.auth.me()` answers 500): the `outcome=error` watch for its group appears in `check_setup`. A normal call adds to `outcome=ok`.

## 4. Alerts to a function

A Base44 webhook trigger (`https://cwebhooks.base44.com/api/hooks/…`) queues the request and answers before your code runs, so it cannot answer the challenge. Use the receiving function's own URL, `https://<app>.base44.app/functions/<name>`, and answer the challenge in it:

```ts
Deno.serve(async (req) => {
  const raw = await req.text(); // the signature covers these exact bytes
  const body = JSON.parse(raw);
  if (body.type === "destination.verify") return Response.json({ challenge: body.data.challenge });
  // … an alert: verify raw against the webhook-signature header with COUNTWATCH_WEBHOOK_SECRET (Standard Webhooks),
  // then act on body
  return new Response(null, { status: 204 });
});
```

Then `manage {"action": "create_destination", "body": {"url": "https://<app>.base44.app/functions/<name>"}}` and `verify_destination`.

## Check it works

1. **Data arrives.** `check_setup` (or `curl -s https://magnum-staging.elirais.workers.dev/v1/status -H "Authorization: Bearer $COUNTWATCH_API_KEY"`): the `app_actions` watch shows `last_request_at`, and `missing_buckets_24h` stays `0` (explicit watches). Do what `next_actions` says.
2. **History is in.** `GET /v1/imports/{import_id}` shows `status: done` and the rules chosen for `app_actions`, with the alerts they would have raised. (Skipped the import? The watch learns for about 22 days; starter rules apply meanwhile.)
3. **An alert reaches you.** The person turns on e-mail alerts in the dashboard (https://magnum-staging.elirais.workers.dev/dashboard), or you add a webhook (`manage create_destination`, then `verify_destination`; the endpoint answers the challenge, see https://magnum-staging.elirais.workers.dev/agent). A write key cannot read alerts: read them with MCP `alerts`, or with `GET /v1/alerts` and an admin key. Then `manage {"action": "send_test_alert"}`: every active destination receives a test alert (`test: true`) within a few minutes.
