Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions umami-postgres/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Thin wrapper around umami's official image at the version this
# sample tracks. Pin lives here (not in CI lane scripts) so a
# future umami release that changes the bug-triggering shape is a
# one-line retag, not a hunt across keploy/integrations and
# keploy/enterprise.
#
# Upstream: https://github.com/umami-software/umami
# Image: docker.io/umamisoftware/umami:postgresql-v2.18.1

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dockerfile comment says the pinned upstream image is docker.io/umamisoftware/umami:postgresql-v2.18.1, but the FROM line uses ghcr.io/umami-software/umami:postgresql-v2.18.1. Please align the comment with the actual registry to avoid confusion when updating the pin.

Suggested change
# Image: docker.io/umamisoftware/umami:postgresql-v2.18.1
# Image: ghcr.io/umami-software/umami:postgresql-v2.18.1

Copilot uses AI. Check for mistakes.
FROM ghcr.io/umami-software/umami:postgresql-v2.18.1
49 changes: 49 additions & 0 deletions umami-postgres/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# umami-postgres — keploy compat lane sample (work in progress)

Minimum reproducer scaffold for the umami / postgres-v3 compat lane. Mirrors the architectural pattern of the [doccano-django sample in `samples-python`](https://github.com/keploy/samples-python/tree/main/doccano-django): the sample owns orchestration (compose / bootstrap / traffic / noise filter / coverage), the keploy CI lanes consume it as a thin wrapper.

## Status

**This is a SCAFFOLD.** The compose, bootstrap, and a minimal record-traffic loop work end-to-end against bare umami without keploy in the picture. The full traffic loop the existing keploy/enterprise lane drives (`run_api_flow` in `enterprise/.ci/scripts/umami-linux.sh`, ~250 lines covering websites / events / sessions / reports / share-tokens / shareability) has **not been ported** into `flow.sh::umami_record_traffic` yet. Lanes consuming this sample today should either:

1. Port the missing curls into `flow.sh::umami_record_traffic` (preferred — that's the migration this scaffold is designed around).
2. Or call into `enterprise/.ci/scripts/umami-linux.sh::run_api_flow` directly between `flow.sh bootstrap` and `flow.sh coverage` until the migration completes.

See the migration plan in this PR's description / linked issue for the full porting checklist.

## Layout

```
umami-postgres/
├── Dockerfile # FROM ghcr.io/umami-software/umami:postgresql-v2.18.1
├── docker-compose.yml # postgres-15 + umami v2 on a fixed subnet, env-driven
├── flow.sh # bootstrap | record-traffic | coverage | list-routes
├── keploy.yml.template # globalNoise for createdAt/updatedAt/Date/uuid id fields
└── README.md # this file
```

## Contract

The sample is keploy-independent: `docker compose up && bash flow.sh bootstrap && bash flow.sh record-traffic` runs end-to-end against bare umami. Lane scripts wrap that exact same path inside `keploy record` / `keploy test`.

* `bootstrap` — login as admin via `/api/auth/login`, capture the JWT-style auth token, persist it to `/tmp/umami-token-${UMAMI_PHASE}` so subsequent calls share a deterministic Authorization header.
* `record-traffic` — drive the umami v1 API. Every call is logged to `${UMAMI_FIRED_ROUTES_FILE}` (when set) so the `coverage` subcommand has a numerator without needing a keploy recording.
* `coverage` — walks the running container's `src/app/api/**/route.ts` tree as the denominator (the umami router is file-system based), compares against fired/recorded routes, emits a `(method, path)` percentage.
* `list-routes` — diagnostic; prints the route table.

## Local run

```sh
docker compose up -d
bash flow.sh bootstrap 240
UMAMI_FIRED_ROUTES_FILE=/tmp/fired.log bash flow.sh record-traffic
UMAMI_FIRED_ROUTES_FILE=/tmp/fired.log bash flow.sh coverage
docker compose down -v
```

## Consumers

Lanes pinning to this sample (pinned via `--branch feat/keploy-compat-lanes-rollout` until merge):

* `keploy/enterprise` `.woodpecker/umami-linux.yml` — being slimmed in a follow-up PR.
* `keploy/integrations` may add a `.woodpecker/umami-postgres.yml` falsifying lane in a future PR (currently no integrations-side coverage of this app).
58 changes: 58 additions & 0 deletions umami-postgres/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# umami-postgres sample compose. Postgres-15 + umami v2 on a fixed
# subnet, every name env-driven so multiple matrix cells can run
# in parallel on the same docker daemon. Two-phase boot pattern
# matches the doccano-django sibling: SKIP_INIT=0 first time so
# umami's `npx umami-app db:up` runs migrations and seeds; volume
# is retained; SKIP_INIT=1 second time launches the app against
Comment on lines +4 to +6

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment describes a SKIP_INIT=0/1 two-phase boot, but the compose file actually uses UMAMI_SKIP_INIT/UMAMI_SKIP_INIT. This mismatch makes it unclear which env var users should set. Consider updating the comment to match the real variable name (or vice versa) so the “two-phase boot” contract is unambiguous.

Suggested change
# matches the doccano-django sibling: SKIP_INIT=0 first time so
# umami's `npx umami-app db:up` runs migrations and seeds; volume
# is retained; SKIP_INIT=1 second time launches the app against
# matches the doccano-django sibling: UMAMI_SKIP_INIT=0 first time so
# umami's `npx umami-app db:up` runs migrations and seeds; volume
# is retained; UMAMI_SKIP_INIT=1 second time launches the app against

Copilot uses AI. Check for mistakes.
# the populated volume.
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: ${UMAMI_APP_CONTAINER:-umami_app}
init: true
stop_grace_period: 5s
ports:
- "${UMAMI_APP_PORT:-3001}:3000"
environment:
DATABASE_URL: postgresql://umami:umami@${UMAMI_DB_IP:-172.35.0.10}:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: ${UMAMI_APP_SECRET:-keploy-fixed-app-secret-for-deterministic-recordings}
DISABLE_TELEMETRY: "1"
DISABLE_UPDATES: "1"
UMAMI_SKIP_INIT: "${UMAMI_SKIP_INIT:-0}"
depends_on:
postgres:
condition: service_healthy
networks:
- umami-net

postgres:
image: postgres:15-alpine
container_name: ${UMAMI_DB_CONTAINER:-umami_db}
stop_grace_period: 5s
environment:
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
POSTGRES_DB: umami
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- umami-db-data:/var/lib/postgresql/data
networks:
umami-net:
ipv4_address: ${UMAMI_DB_IP:-172.35.0.10}

networks:
umami-net:
driver: bridge
ipam:
config:
- subnet: ${UMAMI_NETWORK_SUBNET:-172.35.0.0/24}

volumes:
umami-db-data:
227 changes: 227 additions & 0 deletions umami-postgres/flow.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#!/usr/bin/env bash
#
# flow.sh — keploy-independent orchestration for the umami-postgres
# sample. Modeled on samples-python/doccano-django/flow.sh.
#
# Subcommands:
# bootstrap — log in as admin, install a deterministic auth
# token so record/replay headers match. Runs
# once against a SKIP_INIT=0 launch; idempotent
# on the named volume.
# record-traffic — drive the API: the call sequence whose
# responses we want recorded. Fire-and-forget;
# keploy is the assertion layer at replay.
# coverage — walk umami's route table inside the running
# container, compare against fired routes, emit
# a (method, path) coverage percentage.
# list-routes — print the route table the coverage report
# uses as its denominator (diagnostic).
#
# HANDOFF NOTE: this is a SCAFFOLD. The traffic loop in
# `umami_record_traffic` below is intentionally minimal — it hits
# the API surface enough to prove the sample boots end-to-end
# without keploy. The full traffic loop (the one
# enterprise/.ci/scripts/umami-linux.sh's `run_api_flow` function
# drives, ~250 lines of curls covering websites / events /
# sessions / reports / share-tokens / shareability) needs to be
# ported here. Until then, the keploy lane consuming this sample
# can either:
# (a) call `bash flow.sh record-traffic` then `bash flow.sh
# extra-traffic-from-lane` where the lane defines the
# extra calls inline, OR
# (b) call into `umami-linux.sh::run_api_flow` directly until
# the migration completes.
# See https://github.com/keploy/samples-typescript/issues/<TBD>
# for the migration plan.
set -Eeuo pipefail

UMAMI_APP_PORT="${UMAMI_APP_PORT:-3001}"
UMAMI_APP_CONTAINER="${UMAMI_APP_CONTAINER:-umami_app}"
UMAMI_DB_CONTAINER="${UMAMI_DB_CONTAINER:-umami_db}"
UMAMI_ADMIN_USER="${UMAMI_ADMIN_USER:-admin}"
UMAMI_ADMIN_PASSWORD="${UMAMI_ADMIN_PASSWORD:-umami}"
UMAMI_FIXED_TOKEN="${UMAMI_FIXED_TOKEN:-}" # populated by bootstrap; lane scripts may pre-seed
UMAMI_PHASE="${UMAMI_PHASE:-local}"
Comment on lines +26 to +29

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UMAMI_FIXED_TOKEN is declared but never used. Either wire it into bootstrap/record-traffic (so lane scripts can pre-seed a deterministic token) or remove it to avoid confusion; right now the header comment implies deterministic auth behavior that isn’t implemented in the script.

Copilot uses AI. Check for mistakes.
UMAMI_FIRED_ROUTES_FILE="${UMAMI_FIRED_ROUTES_FILE:-}"

base="http://127.0.0.1:${UMAMI_APP_PORT}"
h_json='Content-Type: application/json'

log_fired() {
[ -z "$UMAMI_FIRED_ROUTES_FILE" ] && return 0
printf '%s %s\n' "$1" "$2" >>"$UMAMI_FIRED_ROUTES_FILE"
}

# umami_wait_for_app — readiness gate. /api/heartbeat returns 200
# only when the Next.js server has bound and Prisma is connected.
# Stronger than wait_for_port; checks the actual app surface.
umami_wait_for_app() {
local timeout=${1:-180}
local start_ts code
start_ts=$(date +%s)
while true; do
code=$(curl -sS -o /dev/null -w '%{http_code}' "${base}/api/heartbeat" 2>/dev/null || echo "")
if [ "$code" = "200" ]; then return 0; fi
if [ $(( $(date +%s) - start_ts )) -ge "$timeout" ]; then
echo "umami_wait_for_app: timed out (last code: ${code:-<empty>})" >&2

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When umami_wait_for_app times out, the error message doesn’t provide a concrete next step to diagnose the failure. Consider including hints like checking docker compose ps, docker logs $UMAMI_APP_CONTAINER, or verifying that UMAMI_APP_PORT matches the compose port mapping to make CI failures easier to debug.

Suggested change
echo "umami_wait_for_app: timed out (last code: ${code:-<empty>})" >&2
echo "umami_wait_for_app: timed out waiting for ${base}/api/heartbeat (last code: ${code:-<empty>}). Next steps: run 'docker compose ps' to confirm services are up, inspect app logs with 'docker logs ${UMAMI_APP_CONTAINER}', and verify UMAMI_APP_PORT=${UMAMI_APP_PORT} matches the compose port mapping." >&2

Copilot uses AI. Check for mistakes.
return 1
fi
sleep 2
done
}

# umami_bootstrap — login as admin via /api/auth/login and capture
# the issued auth token (umami uses JWT-like tokens in the
# Authorization: Bearer header). Stores under
# /tmp/umami-token-${UMAMI_PHASE} so `record-traffic` can read it.
umami_bootstrap() {
local timeout=${1:-180}
umami_wait_for_app "$timeout"

local resp code
resp=$(curl -sS -o /tmp/umami-login.json -w '%{http_code}' \
-H "$h_json" -X POST "${base}/api/auth/login" \
-d "{\"username\":\"${UMAMI_ADMIN_USER}\",\"password\":\"${UMAMI_ADMIN_PASSWORD}\"}" 2>/dev/null || echo "")
if [ "$resp" != "200" ]; then
echo "umami_bootstrap: login failed (code ${resp:-empty})" >&2
cat /tmp/umami-login.json >&2 || true
return 1
fi
local token
token=$(jq -r '.token' /tmp/umami-login.json 2>/dev/null)
if [ -z "$token" ] || [ "$token" = "null" ]; then
echo "umami_bootstrap: no token in login response" >&2
return 1
fi
printf '%s' "$token" > "/tmp/umami-token-${UMAMI_PHASE}"
Comment on lines +90 to +105

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/tmp/umami-login.json is a fixed path. If curl fails before writing the file, cat may print stale output from a previous run, which can mislead debugging. Consider using a temp file (e.g., mktemp) and cleaning it up, or truncating the file before the request so failures don’t surface old content.

Suggested change
local resp code
resp=$(curl -sS -o /tmp/umami-login.json -w '%{http_code}' \
-H "$h_json" -X POST "${base}/api/auth/login" \
-d "{\"username\":\"${UMAMI_ADMIN_USER}\",\"password\":\"${UMAMI_ADMIN_PASSWORD}\"}" 2>/dev/null || echo "")
if [ "$resp" != "200" ]; then
echo "umami_bootstrap: login failed (code ${resp:-empty})" >&2
cat /tmp/umami-login.json >&2 || true
return 1
fi
local token
token=$(jq -r '.token' /tmp/umami-login.json 2>/dev/null)
if [ -z "$token" ] || [ "$token" = "null" ]; then
echo "umami_bootstrap: no token in login response" >&2
return 1
fi
printf '%s' "$token" > "/tmp/umami-token-${UMAMI_PHASE}"
local resp code login_resp_file
login_resp_file=$(mktemp /tmp/umami-login.XXXXXX.json)
resp=$(curl -sS -o "$login_resp_file" -w '%{http_code}' \
-H "$h_json" -X POST "${base}/api/auth/login" \
-d "{\"username\":\"${UMAMI_ADMIN_USER}\",\"password\":\"${UMAMI_ADMIN_PASSWORD}\"}" 2>/dev/null || echo "")
if [ "$resp" != "200" ]; then
echo "umami_bootstrap: login failed (code ${resp:-empty}); verify the app is reachable and the admin credentials are correct, then retry." >&2
cat "$login_resp_file" >&2 || true
rm -f "$login_resp_file"
return 1
fi
local token
token=$(jq -r '.token' "$login_resp_file" 2>/dev/null)
if [ -z "$token" ] || [ "$token" = "null" ]; then
echo "umami_bootstrap: no token in login response; inspect the login API response and confirm the expected token field is present, then retry." >&2
rm -f "$login_resp_file"
return 1
fi
printf '%s' "$token" > "/tmp/umami-token-${UMAMI_PHASE}"
rm -f "$login_resp_file"

Copilot uses AI. Check for mistakes.
echo "umami_bootstrap: token captured for phase ${UMAMI_PHASE}"
}

# umami_record_traffic — SCAFFOLD traffic loop. See HANDOFF NOTE
# at the top of this file. Hits enough of the v1 surface to prove
# the sample boots; the full coverage-extending loop is in
# enterprise/.ci/scripts/umami-linux.sh::run_api_flow and needs to
# be ported here in a follow-up.
umami_record_traffic() {
local token
token=$(cat "/tmp/umami-token-${UMAMI_PHASE}" 2>/dev/null || echo "")
if [ -z "$token" ]; then
echo "umami_record_traffic: no auth token at /tmp/umami-token-${UMAMI_PHASE}; run \`flow.sh bootstrap\` first" >&2
return 1
fi
local h_auth="Authorization: Bearer ${token}"

umami_wait_for_app 60

log_fired GET "$base/api/heartbeat"
curl -sS "$base/api/heartbeat" >/dev/null || true

log_fired GET "$base/api/me"
curl -sS -H "$h_auth" "$base/api/me" >/dev/null || true

log_fired GET "$base/api/teams"
curl -sS -H "$h_auth" "$base/api/teams" >/dev/null || true

log_fired GET "$base/api/websites"
curl -sS -H "$h_auth" "$base/api/websites" >/dev/null || true

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record-traffic currently swallows request failures (curl ... || true), so the command can exit 0 even when the API is down / returning 401s. That makes the scaffold look healthy while not actually exercising the surface (and also logs routes as fired even if the request failed). Consider using curl -f (or checking status codes) and letting the script fail on the first unexpected response; only append to UMAMI_FIRED_ROUTES_FILE after a successful call.

Copilot uses AI. Check for mistakes.

# Create a website so subsequent reads have something to find.
local website_resp website_id
log_fired POST "$base/api/websites"
website_resp=$(curl -fsS -H "$h_auth" -H "$h_json" -X POST "$base/api/websites" \
-d "{\"name\":\"keploy-${UMAMI_PHASE}\",\"domain\":\"sample.keploy.io\"}" 2>/dev/null || echo "")
website_id=$(jq -r '.id // empty' <<<"$website_resp" 2>/dev/null || true)
if [ -n "$website_id" ]; then
log_fired GET "$base/api/websites/${website_id}"
curl -sS -H "$h_auth" "$base/api/websites/${website_id}" >/dev/null || true
log_fired GET "$base/api/websites/${website_id}/stats"
curl -sS -H "$h_auth" "$base/api/websites/${website_id}/stats?startAt=0&endAt=$(date +%s%3N)" >/dev/null || true
fi

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The website create call is wrapped with || echo "", which hides HTTP failures from curl -f and then proceeds with an empty response. This can silently skip the rest of the traffic and still exit 0. Prefer failing hard on a non-2xx response (or explicitly handling expected conflicts like “already exists” by checking the status code and response body).

Suggested change
local website_resp website_id
log_fired POST "$base/api/websites"
website_resp=$(curl -fsS -H "$h_auth" -H "$h_json" -X POST "$base/api/websites" \
-d "{\"name\":\"keploy-${UMAMI_PHASE}\",\"domain\":\"sample.keploy.io\"}" 2>/dev/null || echo "")
website_id=$(jq -r '.id // empty' <<<"$website_resp" 2>/dev/null || true)
if [ -n "$website_id" ]; then
log_fired GET "$base/api/websites/${website_id}"
curl -sS -H "$h_auth" "$base/api/websites/${website_id}" >/dev/null || true
log_fired GET "$base/api/websites/${website_id}/stats"
curl -sS -H "$h_auth" "$base/api/websites/${website_id}/stats?startAt=0&endAt=$(date +%s%3N)" >/dev/null || true
fi
local website_resp website_id website_status website_resp_file
log_fired POST "$base/api/websites"
website_resp_file=$(mktemp)
website_status=$(curl -sS -o "$website_resp_file" -w "%{http_code}" -H "$h_auth" -H "$h_json" -X POST "$base/api/websites" \
-d "{\"name\":\"keploy-${UMAMI_PHASE}\",\"domain\":\"sample.keploy.io\"}")
website_resp=$(cat "$website_resp_file")
rm -f "$website_resp_file"
if [ "$website_status" -lt 200 ] || [ "$website_status" -ge 300 ]; then
echo "umami_record_traffic: website creation returned HTTP ${website_status}; verify the Umami app is healthy and the admin token is valid, then retry \`flow.sh bootstrap\` or rerun this flow" >&2
return 1
fi
website_id=$(jq -r '.id // empty' <<<"$website_resp" 2>/dev/null || true)
if [ -z "$website_id" ]; then
echo "umami_record_traffic: website creation succeeded but no website id was returned; inspect the /api/websites response format and retry the flow" >&2
return 1
fi
log_fired GET "$base/api/websites/${website_id}"
curl -sS -H "$h_auth" "$base/api/websites/${website_id}" >/dev/null || true
log_fired GET "$base/api/websites/${website_id}/stats"
curl -sS -H "$h_auth" "$base/api/websites/${website_id}/stats?startAt=0&endAt=$(date +%s%3N)" >/dev/null || true

Copilot uses AI. Check for mistakes.
}

umami_list_routes() {
# umami exposes its v1 routes via the Next.js file-system
# router. Inside the container, src/app/api/**/route.ts is
# the source of truth. find them and emit (method, path).
docker exec -i "$UMAMI_APP_CONTAINER" sh -c '
cd /app && find src/app/api -name "route.ts" -o -name "route.js" 2>/dev/null | while read f; do
rel="${f#src/app/api/}"
rel="${rel%/route.ts}"
rel="${rel%/route.js}"
grep -oE "export[[:space:]]+(async[[:space:]]+)?function[[:space:]]+(GET|POST|PUT|DELETE|PATCH)" "$f" \
| awk "{print \$NF}" \
| sort -u \
| while read method; do
echo "$method /api/${rel}"
done
done
' 2>/dev/null | sort -u
}

umami_list_recorded_routes() {
local f method route
local found_keploy=0
while IFS= read -r f; do
found_keploy=1
method=$(awk '/^ method:/{print $2; exit}' "$f")
route=$(awk '/^ url:/{print $2; exit}' "$f")
route="${route%%\?*}"
case "$route" in http://*|https://*) route="/${route#*://*/}" ;; esac
if [ -n "$method" ] && [ -n "$route" ]; then echo "$method $route"; fi
done < <(find keploy -type f -path '*/tests/*.yaml' 2>/dev/null) | sort -u
if [ "$found_keploy" = "1" ]; then return 0; fi

if [ -n "$UMAMI_FIRED_ROUTES_FILE" ] && [ -f "$UMAMI_FIRED_ROUTES_FILE" ]; then
while IFS= read -r line; do
method="${line%% *}"; route="${line#* }"
route="${route%%\?*}"
case "$route" in http://*|https://*) route="/${route#*://*/}" ;; esac
[ -n "$method" ] && [ -n "$route" ] && echo "$method $route"
done <"$UMAMI_FIRED_ROUTES_FILE" | sort -u
fi
}

umami_report_coverage() {
local routes_file recorded_file
routes_file="$(mktemp)"; recorded_file="$(mktemp)"
umami_list_routes >"$routes_file"
umami_list_recorded_routes >"$recorded_file"

if [ ! -s "$routes_file" ]; then
echo "WARNING: umami_list_routes produced no rows; skipping coverage report" >&2
rm -f "$routes_file" "$recorded_file"; return 0

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This WARNING: message + return 0 causes coverage to succeed even when route discovery failed (e.g., wrong container name, docker exec failure, or path changes in the image). Since this is used in CI gating, consider treating “no routes discovered” as an error with an actionable next step (e.g., verify the container is running and UMAMI_APP_CONTAINER matches), and exit non-zero instead of emitting a warning.

Suggested change
echo "WARNING: umami_list_routes produced no rows; skipping coverage report" >&2
rm -f "$routes_file" "$recorded_file"; return 0
echo "ERROR: umami_list_routes produced no rows. Verify the app container is running, confirm UMAMI_APP_CONTAINER='${UMAMI_APP_CONTAINER}' matches the actual container name, and check whether the route discovery path inside the image has changed." >&2
rm -f "$routes_file" "$recorded_file"; return 1

Copilot uses AI. Check for mistakes.
fi

local total covered missing pct
total=$(wc -l <"$routes_file" | tr -d ' '); covered=0; missing=""
while IFS= read -r line; do
local method="${line%% *}"
local route="${line#* }"
local pattern="^${method} $(printf '%s' "$route" | sed -E 's/\[[^]]+\]/[^\/]+/g')$"
if grep -qE "$pattern" "$recorded_file"; then
covered=$((covered + 1))
else
missing+=" ${method} ${route}"$'\n'
fi
done <"$routes_file"
if [ "$total" -gt 0 ]; then
pct=$(awk -v c="$covered" -v t="$total" 'BEGIN{printf "%.1f", c*100/t}')
else pct="0.0"; fi
{
echo "================ umami API coverage ================"
echo "Covered ${covered}/${total} (${pct}%)"
if [ -n "$missing" ]; then echo "Uncovered:"; printf '%s' "$missing"; fi
echo "===================================================="
} | tee "${COVERAGE_REPORT_FILE:-coverage_report.txt}"
rm -f "$routes_file" "$recorded_file"
}

case "${1:-}" in
bootstrap) umami_bootstrap "${2:-180}" ;;
record-traffic) umami_record_traffic ;;
coverage) umami_report_coverage ;;
list-routes) umami_list_routes ;;
*)
echo "usage: $0 {bootstrap|record-traffic|coverage|list-routes}" >&2
exit 2 ;;
esac
30 changes: 30 additions & 0 deletions umami-postgres/keploy.yml.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# keploy.yml template for the umami-postgres sample.
#
# Lane scripts copy this into the run dir before invoking
# `keploy record` / `keploy test`. globalNoise covers the
# fields whose value is inherently non-deterministic across
# record/replay (timestamps the server stamps from time.now()
# or generates from random sources):
#
# header.Date
# Set by Next.js / the runtime on every response.
# body.createdAt / body.updatedAt
# Prisma auto-now fields stamped on insert/update.
# body.id (when it's a uuid response field) / body.token
# Server-generated identifiers — the test surface that
# gates correctness lives in the *response shape*, not
# these random values.
#
# Add to this list when umami introduces another auto-stamped
# field; do NOT add it to the lane scripts (that's how the
# noise lists drift between consumers).
test:
globalNoise:
global:
header.Date: []
body.createdAt: []
body.updatedAt: []
body.id: []
body.token: []
body.shareId: []
body.websiteId: []