Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ DATABASE_URL="postgresql://user:password@host:5432/dbname"
# Auth — HMAC secret for signing session cookies (REQUIRED in production).
# Generate with: openssl rand -hex 32
AUTH_TOKEN_SECRET="generate-a-random-secret"
# Adjust API credentials require a stable, server-side encryption key:
# base64 of exactly 32 random bytes. Store the real value in Secret Manager,
# never in source. Unset blocks new connections. Rotation requires a plan to
# re-encrypt existing credentials or reconnect them with their API tokens.
PLATFORM_CREDENTIAL_KEY=""
# Legacy NextAuth env — falls back to AUTH_TOKEN_SECRET if set
NEXTAUTH_SECRET="generate-a-random-secret"
NEXTAUTH_URL="http://localhost:3000"
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jobs:
INGEST_WEBHOOK_SECRET: e2e-competitor-ingest-secret
WORKER_WEBHOOK_SECRET: e2e-worker-secret
INVITE_CODES_DISABLED: 'true'
REPORT_DB_TESTS: '1'
ADJUST_TEST_API_URL: http://127.0.0.1:3322
# Fixture-only encryption key. Never deploy this value.
PLATFORM_CREDENTIAL_KEY: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -66,9 +70,11 @@ jobs:
run: npm run test:e2e

- name: Upload Playwright report
if: failure()
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
path: |
playwright-report/
test-results/
retention-days: 7
147 changes: 147 additions & 0 deletions docs/agent/adjust-setup-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Adjust Setup Validation

Date: 2026-09-24. Release branch `codex-mt/adjust-onboarding`, separated from
the broader report/model work. This document records pre-release verification;
live Adjust account acceptance remains pending the user's token and mappings.

The release includes the minimum signed-session boundary: raw user-ID cookies
are rejected before the new credential routes. Existing signed sessions remain
supported; old raw-ID sessions must log in again. Seedance model/ownership and
the broader dashboard metric changes are not part of this branch.

## Scope and Modules

The first intended live products are Luddi and Cuddler. Their production app
tokens and events are not known or guessed. The same connection can discover
both apps, with independent product association, metrics, source rules,
currency, fixed UTC offset and attribution settings. Acquisition configuration
and credentials are not imported.

- `src/lib/platforms/adjust.ts`: fixed Report Service transport and catalogs.
- `src/lib/reports/adjust-data.ts`: validation, traffic classification and
normalization, with no database or network access.
- `src/lib/reports/adjust-service.ts`: workspace ownership, configuration and
idempotent latest-window snapshot persistence.
- `src/lib/platform-credential.ts`: encrypted credential envelope.
- `src/app/api/adjust/`: connection, catalog, preview, save and sync routes.
- `src/components/adjust-setup.tsx` and `src/components/adjust/`: onboarding,
metric/source configuration, filtered report and CSV export.
- `prisma/migrations/20260924114500_adjust_report_snapshot/migration.sql`:
additive snapshot table; existing report/event records remain untouched.

The UI is at `/settings/adjust`, honoring the deployment basePath. APIs and
reports require a signed session and active workspace; connection, catalog,
preview and writes require owner/admin. Members can read saved reports.

## Acceptance Scenarios

1. Given an authorized workspace, connecting a valid token discovers apps;
storage is encrypted and neither token nor ciphertext is returned by APIs.
2. Given two apps, each uses its own available metrics. A metric from the other
app is rejected. Selected count/user/revenue semantics require confirmation.
3. Given organic, known paid, conflicting or unknown sources, normalization
preserves raw labels/IDs and leaves ambiguity unknown. Exact per-app rules
can override classification; unknown is never treated as organic.
4. Given a saved app, repeated sync replaces one snapshot. Provider failure
preserves the last successful report, while a config change marks it stale.
5. Given a different workspace or anonymous caller, reject access before
contacting the provider. Runtime test endpoint overrides reject production.
Members cannot use either the dedicated or combined sync entry to bypass
the Adjust write restriction.
6. Given an app's opt-in automatic sync, select seven complete reporting dates
in that app's fixed UTC offset, not partially elapsed dates in another zone.

No Adjust spend is added to media spend. Dimension-level unique users are not
summed into app-wide totals. Missing metrics are null, not inferred zeros. This
report does not yet supply a daily warehouse or matched media CPA/CPP/ROAS.

## Local Verification

Working directory: `/Users/mt/.codex/worktrees/adjust-onboarding-release/adex`.

```sh
npm test
npx tsc --noEmit
npm run lint
env -u DATABASE_URL npx prisma generate
env -u DATABASE_URL npx prisma validate
env -u DATABASE_URL -u SEEDANCE2_API_KEY -u ANTHROPIC_API_KEY npm run build
env -u DATABASE_URL -u SEEDANCE2_API_KEY -u ANTHROPIC_API_KEY \
-u WORKER_WEBHOOK_SECRET -u INGEST_WEBHOOK_SECRET PORT=3327 \
npm run test:e2e -- --workers=2 --reporter=dot
```

- Focused Adjust tests: 42 passed in three files, including encryption,
malformed/failed responses, classification, metrics and timezone boundaries.
- Timezone boundary test was observed failing before implementation, then
passing. Negative validation changed the unknown-source fallback to organic:
two tests failed; restoring the intended behavior passed.
- Prisma client generation and schema validation passed without a database.
- `env -u DATABASE_URL npx prisma migrate dev --name adjust_report_snapshot`
failed because `datasource.url` is missing. No migration was applied.
- Scoped release unit run: 470 passed across 38 files, zero skipped. Type check
and production build passed. Lint has zero errors and 35 existing warnings
outside changed files; targeted lint of the final sync permission change
also passed. `git diff --check` passed.
- Local full E2E run: 59 total, 25 passed and 34 skipped. Database-dependent
cases, including all nine Adjust database cases, were skipped without
`DATABASE_URL`. This is not a successful database integration run; the new
member-permission and existing-user-cookie cases still require CI execution.

Browser QA used an isolated Ego Lite task with fictional API responses and a
temporary preview route, now removed. Desktop 1440x1000 and mobile 390x844
verified app switching, per-app events, preview, unknown-source filtering,
CSV download and preserving the report after a 502 failure. Mobile document
width was 390px; the report table scrolled within its container.

Artifacts: `/tmp/adex-adjust-qa-20260924/desktop.png`, `mobile.png`, `unknown.csv`.
These precede the final component extraction, semantic-confirmation checkbox
and CSV metadata additions. They validate interaction/layout with fixtures,
not actual provider data or final database integration.

## Database and Release Gates

The nine database E2E cases in `e2e/adjust-setup.spec.ts` use real Postgres and a
local HTTP provider, not mocked Prisma. They require the existing isolated CI
database with `REPORT_DB_TESTS=1` and `ADJUST_TEST_API_URL=http://127.0.0.1:3322`.
The latter is rejected when `NODE_ENV=production`; never deploy it. CI's
encryption key is fixture-only. The PR's checks must verify these cases before
merge; local no-database checks alone are insufficient.

The DB-backed UI case checks normalized preview values, cached-report access
when the provider catalog fails, saved metric labels and mobile overflow. It
records desktop/mobile screenshots under the workflow's `playwright-report`
artifact, including successful runs. Disconnect tests reject missing or
object-valued filters before any delete and preserve the connection for members.

Before release:

1. Pass the migration and full database E2E suite on an isolated test database.
2. Configure a stable `PLATFORM_CREDENTIAL_KEY` through the deployment secret
manager: base64 of exactly 32 random bytes. Do not put its value in source,
ordinary files or logs. Preserve it across revisions; key rotation requires
a re-encryption/reconnection plan. Missing/invalid keys block new connections.
3. New/replaced Adjust credentials are AES-256-GCM encrypted with workspace
binding. Legacy plaintext credentials remain readable for compatibility;
they are not silently migrated. Reconnect them through the secure page.
4. After release, enter the Adjust Report Service API token only in the secure
settings page. Select the actual Luddi and Cuddler apps; confirm registration
count versus users, unique payers, revenue definition, currency, UTC offset
and attribution settings. Missing unique-user metrics must remain unmapped.
5. Reconcile a fixed date window with Adjust's dashboard, including unknown
source rows, zero/missing values, totals and provider warnings. Only then
opt into existing daily sync. No new scheduler or live automatic run has
been created/enabled by this change.

Fixed UTC offsets do not automatically follow daylight-saving transitions.
No custom external API URL import or Adjust OAuth account login is implemented.
Media-cost joins, durable daily history and cohort/maturity processing remain
separate work under [the reporting contract](../growth/07-media-adjust-reporting.md).

## Recovery

Before the first real Adjust credential is stored, reverting the Cloud Run
revision can leave the additive table and unused encryption key in place.
After encrypted credentials are stored, pre-feature clients cannot use them:
prefer a forward fix rather than blindly returning to the plaintext client.
Never rotate or delete the encryption key as part of a rollback.
135 changes: 135 additions & 0 deletions docs/growth/07-media-adjust-reporting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Media APIs + Adjust Reporting Contract

Date: 2026-09-24. Target source contract with a local Adjust setup implementation;
not a completed live integration or release.

## Source Boundary

Adex reporting should work with media APIs and Adjust, without depending on
data_agent/acquisition configuration, its databases, GA4, or RevenueCat.
Acquisition is a reference for presentation, ratio aggregation and quality
indicators only. Do not import its credentials, event mappings, product IDs,
campaign naming rules, short-link registry, or time corrections.

- Media APIs own spend, impressions and clicks at a single daily fact grain.
Do not add Adjust cost to media cost or sum account/campaign/adgroup copies.
- Adjust owns attributed installs and configured conversion/revenue metrics.
Registration and purchase events must already reach Adjust from the product.
Connecting the reporting API cannot create missing product instrumentation.
- Product-specific behavior can be added only when that product sends a
corresponding Adjust event. Missing metrics stay unavailable, not zero.
- Raw events and aggregate report rows are different facts. Never synthesize
ConversionEvent records from aggregated Adjust metrics.

## Metric Definitions

| Metric | Required evidence |
| --- | --- |
| Spend, impressions, clicks | Media daily facts, currency and reporting timezone |
| Installs | Adjust attributed installs, attribution mode and app identity |
| Registrations and CPA | Configured registration metric with known count/user semantics; spend from matching dimensions and window |
| Paying users and CPP | A matching unique-payer metric; purchase event count and subscription activations are not substitutes |
| Revenue and ROAS | Explicit IAP/subscription/ad-revenue selection, currency, gross/net definition, cohort anchor and window |
| Net-profit ROI | Not available from advertising spend and revenue alone; requires the other business costs and adjustments |

Recompute ratios from matched additive operands. Never average row-level CPA
or ROAS. Do not sum daily unique users to claim range-wide unique users; query
the required reporting period and a compatible provider metric. Keep calendar
revenue separate from cohort revenue, and define renewals/refunds explicitly.

Adjust's documented cohorts start at install or reattribution, not registration.
Its D0 is the first 24 hours; a cumulative D6 covers the first seven days.
Do not relabel an Adjust D7 metric as registration-based 168-hour ROAS. Exact
registration cohorts require suitable timestamped raw Adjust data and stable
identity, plus separate ingestion work; the aggregate API alone is insufficient
evidence for that definition. See [How cohorts work](https://help.adjust.com/en/article/how-cohorts-work).

The report API's `cohort_maturity=mature` can return zeros for immature cohorts.
Persist/derive maturity separately so an unavailable window is not a true zero.
Pin attribution source, reattribution inclusion, currency and timezone rather
than relying on account defaults. Keep SKAdNetwork and other attribution views
separate unless an explicit non-overlapping reconciliation is verified. See the
[JSON report contract](https://dev.adjust.com/en/api/rs-api/reports/).

## Adex-Owned Configuration

For each product, resolve its Adjust app identifiers, media account/campaign
IDs, available registration/payment event metrics, revenue scope, currency,
timezone and attribution/cohort definition. These are product-specific values,
not copied acquisition settings. Secrets remain in the existing credential
store; documentation, fixtures and ordinary configuration must not contain them.

Discover actual metric IDs using Adjust's [Events endpoint](https://dev.adjust.com/en/api/rs-api/events/)
and [Filters Data endpoint](https://dev.adjust.com/en/api/rs-api/filters-data/).
An event token is not necessarily a Report Service metric slug. Availability
must be verified for the connected account and app, not guessed from labels.

Join only compatible facts using product/app, media platform, account/campaign
IDs and reporting period. Include OS/country only when both sources support
those dimensions. Never copy one campaign's total cost into every OS/product
slice. Missing IDs stay unmatched; names are display labels, not join keys.

## Implemented Local Setup

- `/settings/adjust` connects a Report Service API token, discovers accessible
apps/events, and saves a separate configuration for each app. Luddi and
Cuddler are the first intended live acceptance targets, not preconfigured
production app IDs. Connecting an account is not an OAuth login flow.
- New credentials use authenticated encryption. Fixed official endpoints,
workspace-scoped access and admin-only connection/configuration avoid an
arbitrary URL credential forwarder. Tokens are never returned to the UI.
- Preview and sync retain raw network/partner and media IDs, then classify
organic, known paid, explicitly mapped other, or unknown traffic. Conflicts
and unmapped sources remain unknown; exact per-app overrides are supported.
- Event count, unique-user and revenue semantics require explicit confirmation.
Missing mappings/values stay unavailable. Provider app-wide totals are queried
separately rather than summing dimension-level unique users.
- `AdjustReportSnapshot` stores one latest successful requested-window report
per configured app. Repeat sync replaces it; provider failures retain it;
changed configuration marks it stale. This is not a daily fact warehouse.
- Configured apps bypass legacy Adjust account-report writes in manual/daily
sync. Automatic sync is opt-in and uses the latest seven complete dates in
each app's configured fixed UTC offset. No scheduler was activated.

See [local validation and release gates](../agent/adjust-setup-validation.md).

## Remaining Gaps

- Migration, database integration tests and real account reconciliation remain
unverified locally. Catalog availability and selected metric semantics must
be checked against the actual Luddi/Cuddler accounts before trusting metrics.
- The new report does not yet join media costs or calculate matched CPA, CPP,
or ROAS. Independent daily facts, cohort windows and historical snapshots
remain follow-up work; do not treat the latest-window snapshot as those facts.
- Unconfigured legacy Adjust connections retain their existing rolling account
snapshot path. No historical rows were reinterpreted or backfilled.
- `growth/adjust-ingest.ts` currently emits zero revenue and uses a callback
user key that can change after registration. It is not an Adjust-only payment
or registration-cohort implementation.
- Existing growth snapshots can mix GA4/RevenueCat/Adjust events, lack complete
product separation, and have their own historical UTC-day revenue windows.
Do not expose them as newly validated Adjust-only metrics or silently backfill
them with a different meaning.
- Existing media writers do not provide every matching dimension (notably OS)
or a complete currency contract. Fine-grained matched costs may be unavailable.
- `getCohortReport` retains a legacy metric selection. No claim is made that it
supplies configured registration metrics or the required cumulative windows.

## Integration Acceptance Gate

Before enabling the new source mode, use sanitized fixtures based on the target
app's available fields, then an explicitly authorized read-only reconciliation:

1. Verify event availability, unique-user semantics, app/account/campaign IDs,
timezone, currency, attribution settings and revenue scope.
2. Persist independent daily media and Adjust aggregate facts with idempotent
keys; query them without multiplying spend or mixing legacy event snapshots.
3. Test missing mappings, unmatched dimensions, currency mismatch, immature
windows, unknown values, provider errors and repeated synchronization.
4. Reconcile a fixed mature date range with both source dashboards, including
unmatched totals and freshness. Do not equate a connected account or HTTP
200 with correct registrations, payer counts or revenue.

No live Adjust queries, real credentials or production writes were used.
A local additive schema/migration is prepared but has not been applied to a
database. API documentation was checked on 2026-09-24.
Loading
Loading