Skip to content

better-sidebar#60

Closed
ahmetskilinc wants to merge 4 commits intodatabuddy-analytics:stagingfrom
ahmetskilinc:better-sidebar
Closed

better-sidebar#60
ahmetskilinc wants to merge 4 commits intodatabuddy-analytics:stagingfrom
ahmetskilinc:better-sidebar

Conversation

@ahmetskilinc
Copy link
Copy Markdown

Pull Request

Description

Please include a summary of the change and which issue is fixed. Also include relevant motivation and context.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@vercel
Copy link
Copy Markdown

vercel bot commented Aug 2, 2025

@ahmetskilinc is attempting to deploy a commit to the Databuddy Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Aug 2, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@izadoesdev
Copy link
Copy Markdown
Member

/3dayslater

@izadoesdev izadoesdev closed this Aug 19, 2025
izadoesdev added a commit that referenced this pull request Apr 7, 2026
- basket: loop HTML tag strip in sanitizeString to defeat stacked-tag
  bypass (js/incomplete-multi-character-sanitization, alert #43)
- tracker: replace Math.random() fallback in generateUUIDv4 with
  crypto.getRandomValues() so downstream IDs are cryptographically
  random (js/insecure-randomness, alerts #59 and #60)
izadoesdev added a commit that referenced this pull request Apr 8, 2026
* evals

* cleanup

* cleanup copywriting

* fix(tracker): audit fixes — cleanup, flush safety, unload reliability

- Fix outgoing links bypassing shouldSkipTracking when disabled/bot
- All plugins (interactions, scroll-depth, errors) now return cleanup
  functions, wired into destroy() via cleanupFns
- Fix HttpClient double-read of response body (use text+parse instead)
- Fix flush race condition: check isFlushing before clearing timer
- destroy() flushes all queues via sendBeacon before clearing
- handlePageUnload uses sendBeacon with fetch fallback for all queues
- databuddyOptIn reinitializes tracker without requiring page reload
- Cache timezone at init instead of creating Intl.DateTimeFormat per event
- Add regression tests for all fixed bugs

* chore: clean up .env.example

Remove unused R2 storage and Logtail env vars.

* chore: modernize turbo.json

Upgrade to schema v2, switch to strict envMode with explicit globalEnv,
simplify task configs by removing redundant fields.

* chore: standardize script names and clean root deps

Rename typecheck/type-check to check-types across packages, use turbo
for test runner, remove unused root dependencies (opentelemetry, maxmind).

* refactor(mapper): simplify import system and adapter API

Replace adapter class pattern with plain mapUmamiRow function. Add
createImport helper that handles session exit detection. Remove old
test script, csv-parse/zod/drizzle deps, and utils-map-events.

* chore(sdk): rename test file .spec.ts to .test.ts

Align with bun test glob pattern (tests/*.test.ts).

* chore: remove unused gitconfig

* chore: upgrade turbo to 2.9.3 and enable future flags

Enable affectedUsingTaskInputs, watchUsingTaskInputs, and
filterUsingTasks to prepare for Turbo 3.0.

* ci: optimize all GitHub Actions workflows

- ci.yml: split into 3 parallel jobs (lint, check-types, test), add
  concurrency group, path-ignore for docs, pin bun to 1.3.4, add
  postgres service, remove redundant full build step
- health-check.yml: add concurrency group, restrict triggers to
  Dockerfile and app source changes only
- docker-publish.yml: switch to Blacksmith Docker tools
  (setup-docker-builder, build-push-action, stickydisk), use native
  arm64 runners instead of QEMU emulation, add concurrency group,
  downsize manifest runners to 2vcpu
- codeql.yml: use Blacksmith runner, add staging branch, add
  concurrency group, remove boilerplate
- dependency-review.yml: add staging branch, use Blacksmith runner

* fix(ci): replace tsgo with turbo check-types, make lint non-blocking

- Root check-types now delegates to turbo (packages already use tsc)
- Lint set to continue-on-error until 166 pre-existing errors are fixed

* fix(ci): add build dependency to check-types turbo task

check-types needs package dist outputs to resolve cross-package types

* fix(ci): make check-types non-blocking until pre-existing errors fixed

* fix(docker): use turbo prune in api.Dockerfile to avoid building dashboard

turbo build --filter=@databuddy/api... was resolving to 21 packages
(including dashboard) due to ^build dependency traversal. turbo prune
correctly scopes to only the 14 actual API dependencies.

* chore: remove unused cursor skills symlink

* chore: add dependabot configuration

* chore(vscode): use biome as typescript formatter

* fix(docker): standardize bun images to 1.3.4-slim

* fix(dashboard): rename SDK clientId to apiKey

* fix(dashboard): update flag SDK method names

* fix(basket): improve geo-ip test resilience for CI

* fix(status-page): improve OG image accuracy and null safety

* refactor(evals): improve code quality and redesign UI

* refactor: improve type safety and code quality across codebase

* style: apply biome linting and formatting

* fix(dashboard): react-doctor fixes and ultracite v7 upgrade (#381)

* chore(notifications): add test scripts to package.json

* test(notifications): add BaseProvider unit tests

* test(notifications): add uptime template tests

* test(notifications): add anomaly template tests

* feat(docker): add self-hosting support with docker-compose configuration (#375)

* feat(docker): add self-hosting support with docker-compose configuration

* feat(docker): update docker-compose for production readiness and security enhancements

* feat(docker): enhance docker-compose with required APP_URL and GEOIP_DB_URL configurations

* feat: add healthchecks to Dockerfiles for api, basket, links, and uptime services (#380)

* fix(docker): update selfhost compose comment to reflect built-in healthchecks

* fix(docker): remove healthchecks, bump bun to 1.3.11, standardize Dockerfiles

- Remove healthcheck.ts and all HEALTHCHECK instructions from Dockerfiles
- Bump all bun images from 1.3.4 to 1.3.11
- Add missing bun.lock copy to basket and uptime for deterministic installs
- Add missing tsconfig.json copy to uptime
- Standardize minify flags (--minify-whitespace --minify-syntax) across all services
- Standardize --outfile to absolute path across all services
- Fix EXPOSE placement (before CMD, not after)
- Fix selfhost compose: use edge tag (latest only exists on release), make APP_URL optional
- Remove unused redis_queue_data volume and databuddy-network from dev compose

* fix(docker): clean up selfhost compose, add compose-level healthchecks

- Remove verbose comments, keep the file self-documenting
- Add healthchecks to all app services using bun -e inline fetch
- Healthchecks live in compose (not baked into images)

* fix(docker): remove noisy required-var syntax from selfhost compose

* fix(docker): use env vars for connection strings instead of hardcoding

* fix(docker): harden selfhost compose for production

- Add log rotation (10m max, 3 files) via shared YAML anchor
- Remove default passwords — force users to set POSTGRES_PASSWORD,
  CLICKHOUSE_PASSWORD, REDIS_PASSWORD in .env
- Use REDISCLI_AUTH env var for redis healthcheck instead of leaking
  password in the command
- Add IMAGE_TAG env var (defaults to edge) so users can pin versions

* style(docker): format selfhost compose

* Cleanup

* fix(basket): fix billing enforcement, desloppify routes and event service

- fix(billing): use Autumn SDK response.allowed instead of broken 150%
  threshold that never enforced limits; disable enforcement for now
  (metering only, free plan users not blocked)
- refactor: extract shared buildTrackEvent builder, eliminate duplicate
  field mapping between insertTrackEvent and processTrackEventData
- refactor: DRY formatDate and getWebhookConfig across webhook files
- refactor: replace verbose new Response(JSON.stringify(...)) with
  native Response.json() across all basket routes
- refactor: remove unnecessary context-as casts from Elysia handlers
- refactor: remove dead BillingBlocked type and "exceeded" branches

* test(basket): comprehensive test suite — 41 → 414 tests

P0 (pure functions): sanitizeString, validateSessionId, validateNumeric,
parsePixelQuery, parseTimestamp, parseEventId, buildBasketErrorPayload,
rethrowOrWrap, verifyStripeSignature, all Zod schemas (track, analytics,
vitals, errors, custom events, outgoing links)

P1 (mocked business logic): checkAutumnUsage, saltAnonymousId,
checkDuplicate, validateRequest, checkForBot, detectBot, parseUserAgent

Integration: HTTP route tests for POST /, /batch, /vitals, /errors,
/events, /track, GET /px.jpg, /health with response contract assertions

Field mapping: buildTrackEvent snapshot with all 47 fields, sanitization
boundary tests (XSS, control chars), output shape completeness check

Shared test helpers: table-driven runners (cases, truthTable, schemaTable),
IP generators, XSS payloads, request factory

* fix(basket): mock shared bot-detection in user-agent tests

The detectBot tests were calling the real @databuddy/shared/bot-detection
which returns different results across environments. Mock it to test only
the wrapper's legacy category mapping logic (AI Crawler, AI Assistant,
Known Bot) which is the only code user-agent.ts owns.

* ci: bump Bun to 1.3.11, fix auth tests to mock DB deps

- Bump CI Bun version from 1.3.4 to 1.3.11 to match local
- Mock @databuddy/db and @databuddy/redis in request-validation tests
  so @hooks/auth can load without a live database connection

* fix(basket): remove evlog mock that leaked across test files

The mock.module("evlog") in request-validation.test.ts replaced the real
EvlogError class, breaking instanceof checks in structured-errors.test.ts
and integration.test.ts when Bun ran them in the same process. The auth
functions don't need evlog mocked — only DB and Redis.

* cleanup

* fix: ultracite

* docs: add sidebar refactor design spec

Provider + shared navigation renderer approach to eliminate
3x duplicated data-fetching and rendering logic in sidebar.

* docs: add sidebar refactor implementation plan

8 tasks: provider, renderer, layout integration, 3 file simplifications,
icon import optimization, and smoke testing.

* chore: remove spec and plan docs

* feat(dashboard): add SidebarNavigationProvider and NavigationRenderer

* refactor(dashboard): simplify sidebar files to consume context

sidebar.tsx, category-sidebar.tsx, and mobile-sidebar.tsx now read all
shared navigation state from SidebarNavigationProvider via
useSidebarNavigation(). Removes duplicated hooks (useWebsitesLight,
useMonitorsLight, useFlags, useHydrated), the per-file categories useMemo
computations, and the manual navigation .map() rendering blocks. Uses
NavigationRenderer in place of the inline nav rendering in both
sidebar.tsx and mobile-sidebar.tsx. All props removed from CategorySidebar
and MobileSidebar; Sidebar no longer passes them.

* perf(dashboard): switch navigation-config to direct icon imports

* ultracite fix

* fix: resolve biome format/check import ordering conflict

- Disable assist/source/organizeImports in biome.json (formatter handles it)
- Fix unused pathname import in mobile-sidebar

* fix: align biome catalog version and apply lint fixes

- Update @biomejs/biome catalog entry from 2.2.2 to 2.4.10
- Import reordering from biome fixAll across dashboard files
- Fix Function type lint warnings in basket test

* cleanup

* fix: ultracite lint

* perf(dashboard): bundle size and rendering optimizations

- Remove framer-motion dep, standardize on motion/react (saves ~30-40KB)
- Replace motion.span in Input/Textarea with CSS transitions
- Replace framer-motion accordion in NavigationSection with CSS grid
- Remove redundant usePathname() from NavigationItem, pass as prop
- Add loading.tsx skeletons for main layout and websites page
- Lazy-load WebsiteDialog via next/dynamic
- Remove unused defaultQueryClientOptions from providers

* perf(dashboard): defer command search, deduplicate session, remove searchParams, codemod icons

- Defer useWebsites() in CommandSearch until dialog opens (enabled: open)
- Remove useSearchParams() from SidebarNavigationProvider (unused, caused re-renders)
- Create shared useSession() hook using TanStack Query, replace authClient.useSession()
  nanostore in sidebar components to eliminate duplicate /get-session requests
- Codemod all ~229 barrel @phosphor-icons/react imports to direct /dist/ssr/ imports
- Fix ActivityIcon → PulseIcon alias (no standalone SSR module)
- Fix malformed XIcon import path in og-preview

* fix(dashboard): gate sidebar queries on isHydrated to prevent hydration mismatch

useSession() from TanStack Query can return cached data on first client
render (populated by FlagsProviderWrapper). This caused monitors/websites
queries to resolve before hydration completed, showing different nav items
than the server. Gate enabled on isHydrated to ensure consistent loading
state during hydration.

* fix(dashboard): use csr icon imports in client components to fix hydration

SSR icon imports (@phosphor-icons/react/dist/ssr/) use a different render
path than client components, causing SVG path mismatches during hydration.
Switch all 250 "use client" files to /dist/csr/ imports. Keep /dist/ssr/
only in the 26 non-client files where it's correct.

* fix(dashboard): revert icon imports to barrel — direct imports cause hydration mismatches

The /dist/ssr/ and /dist/csr/ icon imports produce different SVG paths
between server and client renders, causing hydration failures. Revert
all 276 files back to the barrel import from @phosphor-icons/react
which handles SSR/CSR internally without mismatches.

* fix(dashboard): revert session dedup — use authClient.useSession() everywhere

authClient.useSession() already handles deduplication and reactivity
via Better Auth's internal session provider. The TanStack Query wrapper
caused hydration mismatches because it returned cached data before
hydration completed. Revert all sidebar components and providers back
to authClient.useSession().

* fix(sdk): use automatic JSX runtime in build config

esbuild defaulted to React.createElement which fails in React 19
environments where React isn't a global. Set jsx: "automatic" in
the unbuild esbuild config to emit react/jsx-runtime imports.

* fix(dashboard): show loading nav until session resolves to prevent hydration mismatch

When the query is disabled (user is null), TanStack Query returns
isLoading: false, causing the sidebar to render "Add Your First Website"
instead of the loading state. Add !user check alongside !isHydrated and
isLoading so the loading navigation shows until session resolves AND
data has loaded.

* perf(dashboard): make sidebar navigation fully static

Remove dynamic website/monitor list fetching from the sidebar. The
sidebar now uses static navigation links (All Websites, All Monitors)
instead of fetching and listing each item individually.

Benefits:
- Zero hydration mismatches (sidebar is identical on server and client)
- 2 fewer API calls per page load (websites.list + uptime.listSchedules)
- No loading states needed in sidebar
- Simpler code (removed createDynamicNavigation, loading builders,
  isHydrated/user guards in populatedConfig)

The website/monitor lists are accessible from their dedicated pages.
Command search still shows individual websites when the dialog is open.

* refactor(dashboard): merge Websites into Overview section in sidebar

* refactor(dashboard): unite Monitors and Status Pages into single section

* fix(auth): add rate limiting to all email-sending callbacks and auth endpoints

Prevents invitation spam and email abuse by adding two layers of protection:
- Redis-backed secondary storage for Better-Auth's built-in IP rate limiting
- Per-email/org Redis rate limits on all email-sending callbacks

* fix(auth): remove secondaryStorage to fix 401s on staging

secondaryStorage caused Better-Auth to look up sessions in Redis
instead of PostgreSQL, breaking all existing sessions. Use database
storage for rate limiting instead.

* fix(auth): use Redis customStorage for rate limiting instead of database

Uses rateLimit.customStorage to store rate limit data in Redis without
affecting session storage. Keys auto-expire after 5 minutes.

* fix(auth): parse JSON in rate limit customStorage getter

The get callback returned a raw string but Better-Auth expects
{ key, count, lastRequest }. Parse the JSON that set() stores.

* fix ultracite lint

* refactor(uptime): merge duplicate UptimeData construction and remove assertion

Deduplicated checkUptime which built near-identical UptimeData objects in
both the success and failure branches. Now builds one object with
conditional field assignment. Also replaced `as { ip: string }` cast
in getProbeMetadata with a runtime typeof check.

* refactor(uptime): simplify Kafka producer

Removed ProducerConfig interface, defaultConfig const, and
getDefaultProducer wrapper. Environment variables are now read
directly in connect(). Lazy init uses nullish coalescing assignment.

* refactor(uptime): remove commented-out sampling config

* refactor(uptime): replace type assertion with honest cast

Changed `(err as { status?: number }).status` to
`(err as Record<string, unknown>).status` after the typeof guard.

* refactor(uptime): remove unused schema, no-op wrapper, and redundant casts

- Delete unused UptimeSchema and elysia `t` import from types.ts
- Remove no-op record() wrapper from tracing.ts (unused _name param)
- Remove redundant `as` casts in mergeWideEvent and captureError
- Remove unused @databuddy/services dependency from package.json

* fix(dashboard): pass organizationId for all-websites custom events query

The "all websites" mode passed empty queryOptions {}, causing
effectiveId to be undefined and the TanStack Query enabled guard
to permanently block the request. The UI showed an infinite loading
state because isPending was always true.

* perf(api): optimize custom events WHERE clause for org-level queries

Org-level queries used (owner_id = X OR website_id = X OR website_id
IN (...)), which prevented ClickHouse from using the primary key
(owner_id, event_name, timestamp) efficiently. Since owner_id is
always set to organizationId at ingestion, the OR was redundant.

Now uses owner_id = {projectId} for a clean primary key scan.
Also removes the PostgreSQL roundtrip to fetch all website IDs.

* perf(dashboard): lazy-load property queries on custom events page

Split the 7-query batch into two: 4 essential queries (summary,
events, trends, trends_by_event) fire immediately and unblock the
page; 3 heavy property queries (classification, distribution,
top_values) load in a separate batch without blocking the main UI.

The property queries use arrayJoin + regex and are the slowest part
of the custom events page load.

* feat(dashboard): rebuild onboarding as 4-step wizard

Replace the old 2-step onboarding with a proper wizard flow:
- Step 1: Add Website (inline form, auto-advances if website exists)
- Step 2: Install Tracking (placeholder, wired up next commit)
- Step 3: Invite Team (email + role input, sent invites as badges)
- Step 4: Explore (feature cards linking to dashboard pages)

Consistent h-12 header/footer bars, soft-gated steps, single
navigation bar (no duplicate controls), router.replace on completion.

* feat(dashboard): add AI-first tracking install step with branded copy cards

Install tracking step with 3 tabs (AI-first ordering):
- Install with AI: branded cards for Claude, Cursor, Copilot, Windsurf
  with real Simple Icons logos and one-click prompt copy
- SDK Package: npm/yarn/pnpm/bun with syntax-highlighted code blocks
- Script Tag: CDN snippet with copy button

Comprehensive agent prompt covers: framework-specific install, all 17
config options, custom events, Network tab verification, CSP, adblockers,
domain mismatch, localhost behavior, and telemetry reporting.

* feat(api): add agent install telemetry endpoint

POST /public/v1/agent-telemetry — collects feedback from AI agents
that install Databuddy via the onboarding prompt.

- agent_install_telemetry table with status, framework, issues, steps
- websiteId auth: validates website exists (Redis-cached with negative cache)
- Rate limited: 10 req/hour per websiteId via sliding window
- Full evlog coverage: wide events for all outcomes (success, rejection,
  rate limit, errors) with structured fields

* feat(dashboard): add custom event tracking to onboarding flow

Track every meaningful onboarding interaction:
- onboarding_started, step_viewed, step_completed, skipped, completed
- onboarding_website_created (with domain)
- onboarding_tracking_copied (with block + method: ai/sdk/script)
- onboarding_tracking_check_status, tracking_verified
- onboarding_invite_sent (with role + count)

Each step updates the URL with ?step={id} for pageview tracking.
Also removes dev step switcher (was left from earlier iteration).

* fix(dashboard): remove high-cardinality domain from onboarding event

Domain is PII-adjacent and high-cardinality — violates event design
rules. The event alone (onboarding_website_created) is sufficient.

* feat(uptime): add manual check button with rate limiting

Adds a "Check Now" action to monitor detail page and row dropdown that
triggers an immediate uptime check via QStash. Rate-limited to 5 per minute
per schedule.

* feat(dashboard): add per-event breakdown to custom events trend chart

Replaces single aggregate area chart with a dual-mode trend chart
supporting aggregate and by-event views. Adds area/bar toggle,
interactive legend with click-to-hide, and inline tooltip sorted by value.

* docs: remove coming-soon placeholders and add integration pages

Remove Pulse placeholder page, real-time analytics and funnel/journey
coming-soon sections from dashboard docs, unused examples sidebar section,
and add missing integration framework entries.

* docs(sdk): consolidate feature flags hooks API around useFlag/useFlags

Replace useFeature, useFeatureOn, useFlagValue, and useVariant references
with the unified useFlag hook and useFlags context API. Remove migration
section and redundant advanced context section.

* chore(sdk): alphabetize flags-manager type imports

* Delete WORKSPACE.md

* chore(dashboard): remove LLM analytics pages

Removes the unused per-workspace and per-website LLM analytics surfaces
(routes, components, demo pages) along with their references in
tool-display labels and chart-preferences locations.

* refactor(api,dashboard): use device_type column for device categories

The devices builder now reads the user-agent-derived device_type column
directly instead of bucketing screen_resolution heuristically. The
mapDeviceTypes plugin and the resolution lookup table go away, and the
device-type filter and dashboard cell are updated to handle the new
categories (desktop, mobile, tablet, smarttv, console, xr, embedded).

Also collapses the two near-duplicate aggregation helpers in
query/utils.ts into a single aggregateRows() and inlines a few
single-use helpers in simple-builder.ts.

* refactor(dashboard,docs): switch favicons from DuckDuckGo to Google S2

DuckDuckGo's icon CDN has been intermittently slow and missing modern
brands. Google S2 is more reliable and supports explicit size buckets
(16/24/32/48/64/96/128/256), so we round-up size*2 for crisp DPR=2
rendering. Updates the image hostname allowlist and CSP img-src in both
dashboard and docs next configs.

Also tightens favicon-image.tsx: extracts hostname/validation helpers,
uses cn() for class composition, and drops the absolute-positioned
fallback wrapper now that the GlobeIcon centers itself.

* feat(monitors): allow editing monitor name and surface timeout/cacheBust

The uptime updateMonitor procedure now accepts an optional name (trimmed
to null when empty) and returns it alongside the schedule. The dashboard
monitor sheet passes the name through on edit, and the monitors list
plus detail page expose timeout and cacheBust on their Monitor /
ScheduleData types so they round-trip into the form correctly.

* refactor(dashboard): consolidate compact number formatting in lib/formatters

formatMetricNumber and the dozen+ ad-hoc formatNumber / formatCompactNumber
helpers scattered across rows, tabs, charts, stat cards, and utility files
all behaved identically in spirit (compact 1.2K/3.4M output) but diverged
in details: fallback strings, NaN handling, locale, max fraction digits.

Replace lib/formatters.ts:formatMetricNumber with a single formatNumber
backed by Intl.NumberFormat compact notation, and import it everywhere
the local copies lived. No behavior change is intended beyond consistency.

* refactor(dashboard): simplify dby/l/[slug] link page

Spread dbLink into the cached link and only override expiresAt instead
of listing every field by hand, and inline the OG image URL builder now
that it has a single caller.

* fix(auth): trim rate-limit TTL and surface throttled callbacks in evlog

Changes the better-auth customStorage TTL from 300s to 120s to match 2x
the longest configured window (60s), so counters survive the full window
without lingering unnecessarily.

Adds log.warn wide-event coverage to every email callback that silently
returned on rate-limit failure (reset_password, verify_email,
verification_otp, magic_link, invitation). Operators can now trace
throttled auth emails via evlog instead of flying blind.

* refactor(mcp): introduce defineMcpTool wrapper and standardize tool surface

Centralize auth, website resolution, per-tool rate limiting, evlog wide-event
coverage, and error envelope mapping behind a single defineMcpTool() factory.
Re-register all 13 existing MCP tools through the wrapper so each handler is
just business logic — try/catch, trackToolCompletion, and access checks live
in one place.

- New apps/api/src/ai/mcp/define-tool.ts: McpToolError class, McpToolMeta /
  McpHandlerContext / McpToolFactory types, defineMcpTool() factory enforcing
  snake_case names + 240-char description cap. Supports resolveWebsite:
  true | "optional" | false.
- New apps/api/src/ai/mcp/tool-context.ts: extracted ensureWebsiteAccess,
  resolveWebsiteId, getOrganizationId, buildRpcContext, coerceQueriesArray
  from tools.ts so the wrapper can call them without circular imports.
- Rewrite apps/api/src/ai/mcp/tools.ts: every tool now goes through
  defineMcpTool. Tightened descriptions to imperative one-liners under the
  cap. Added per-tool rate limits (ask 10/min, get_data 30/min, memory tools
  30/min, others 60/min). Funnel/goal tools accept new from/to with
  startDate/endDate aliased for back-compat. List tools return a hint on
  empty results. Errors now thrown as McpToolError so the wrapper handles
  the envelope uniformly.
- Modify apps/api/src/routes/mcp.ts: drop the hardcoded toolIds list and the
  isMemoryEnabled() registration branch. The route iterates whatever
  createMcpTools(ctx) returns; adding a tool is now a one-file change.

Wide event fields per call: mcp_tool, mcp_auth_type, mcp_status,
mcp_duration_ms, mcp_error_code, mcp_rate_limited, mcp_website_id.

* feat(mcp): add 5 insight tools (list, summarize, compare, top_movers, detect_anomalies)

Expose Databuddy insights through MCP so agents can answer "what changed",
"what's interesting", and "any issues" without composing get_data queries
manually. Each tool returns both flat numbers and a one-line headline so the
agent can scan or drill down depending on token budget.

list_insights — read pre-computed insights from analyticsInsights table.
  Org-wide by default; pass websiteId/Name/Domain to scope. New 'since'
  shorthand (last_24h | last_7d | last_30d | last_90d). Sorts by priority
  desc, createdAt desc. Cached 60s via cacheable() with org-id list in key.

summarize_insights — compact triage view: counts by severity/type/sentiment/
  website plus the top 3 priorities (~80 tokens vs ~2000 for list_insights).
  Org-wide by default, defaults to last_7d.

compare_metric — period-over-period diff for one of visitors, sessions,
  pageviews, bounce_rate, session_duration, events. Auto-computes the
  previous period (same length, immediately preceding). Returns current,
  previous, delta, deltaPercent, direction, isImprovement (flips for
  bounce_rate where lower is better), and a headline like
  "Visitors up 12.4% (12,480 vs 11,100)". Replaces two manual get_data
  calls and the math.

top_movers — top dimension rows that changed most between two periods for
  pages/referrers/countries/browsers/os. Fetches top 100 of each period,
  merges by name, sorts by abs(delta). Handles new entries (previous=0)
  and dropouts (current=0) with explicit headlines. Filters by direction
  (up/down/both).

detect_anomalies — z-score check on daily summary metrics over the last
  7-60 days. Pulls events_by_date, computes baseline mean+stddev across
  prior days, flags the latest day if |z| > threshold (default 2.0).
  Skips metrics with insufficient samples or zero variance. Returns
  metrics sorted by |z| desc with structured output and headlines.

Plumbing:
- New apps/api/src/lib/insights-query.ts: fetchInsightsForOrgs() taking
  organizationIds[] (multi-org for session users), with filters for
  type/severity/sentiment/createdAfter/createdBefore. Joins websites to
  filter soft-deleted.
- New apps/api/src/ai/mcp/insights-tools.ts: METRIC_REGISTRY,
  DIMENSION_REGISTRY, period helpers (resolvePeriodRange,
  computePreviousPeriod, parseSinceShorthand), format helpers
  (formatMetricValue, buildMetricHeadline, buildDimensionHeadline,
  buildAnomalyHeadline), stats helpers (mean, stddev), and the 5 tool
  factories.
- apps/api/src/ai/mcp/tool-context.ts: new resolveOrganizationIds()
  helper — single source of truth for org resolution policy
  (website → API key org → user memberships).
- apps/api/src/ai/mcp/define-tool.ts: resolveWebsite now accepts
  "optional" mode so list_insights/summarize_insights can run org-wide
  while still validating access when a selector is passed.
- apps/api/src/ai/mcp/tools.ts: imports INSIGHT_TOOL_FACTORIES and spreads
  into CORE_TOOL_FACTORIES; capabilities tool advertises each new tool
  with usage hints.

Rate limits: list_insights/summarize_insights/compare_metric/top_movers
60/min, detect_anomalies 30/min.

MCP tool count: 17 (was 12 after the wrapper refactor).

* feat(docs): add /oss program page for open source maintainers

One year of Databuddy Pro, free, for active OSS project maintainers.
Narrow letter-style page with react-hook-form + zod, accelerator select,
and Slack webhook submission via /api/oss/submit.

* chore: ignore .gstack/

* perf(dashboard): skip property queries on org-level events view

Property queries (classification/distribution/top_values) do
arrayJoin(JSONExtractKeys(properties)) over every event row. On
org-level scans this is the dominant cost and can take 30s+. Skip
them when no website is selected; the SummaryView already shows a
"no properties" empty state which is acceptable for the cross-website
view.

* refactor(uptime): simplify pingWebsite and bump timeout to 60s

Drop the brotli-first + gzip/deflate retry dance and the encoding
failure detection. Always fetch with gzip/deflate, which handles the
monitors we actually have without the fallback complexity. Raise the
default timeout from 30s to 60s so slower targets don't get false
positives.

* feat(mcp): add tool output schemas and cache website lookups

- define-tool: support `outputSchema` meta and emit `structuredContent`
  (MCP 2025-06-18 Tool Output Schemas) for clients that want typed data.
  Factory now carries `toolName` to let the registry self-list.
- routes/mcp: forward per-tool outputSchema to the MCP SDK.
- tools/insights-tools: declare output schemas for every tool, drop
  deprecated `startDate`/`endDate` aliases on funnel/goal analytics,
  and report `truncated` from top_movers when the fetch cap is hit.
- tool-context: add `getCachedAccessibleWebsites` (Redis, 30s TTL)
  keyed by principal id, and consolidate `ensureWebsiteAccess` here.
- agent-tools: drop the duplicate access-check and reuse the shared
  helper.
- tools: register memory tools inline in `ALL_TOOL_FACTORIES`, derive
  `capabilities.availableTools` from the registry so it can't drift,
  and compact tool hints. `save_memory` now returns `{queued:true}`
  to reflect the fire-and-forget storage.

* feat(dashboard): public mode for BillingProvider

Skip all billing fetches on demo/public routes and serve FREE-plan
defaults directly, so unauth visitors don't trigger customer/plan
requests. canUserUpgrade stays true so upgrade CTAs still lead to
signup. Demo layout also gains SidebarNavigationProvider and
CommandSearchProvider to match main.

* style(dashboard): compact events stream filter toolbar

Switch the filter bar to a dense h-7 layout using shared Badge
primitives and muted-foreground tokens instead of primary tints,
so the active-state stays obvious without dominating the view.
Search input gets an inline clear button and count moves to a
gray Badge.

* refactor(dashboard): map browser/os icons to explicit file extensions

Replace the hard-coded Brave/QQ webp special case with a per-icon
extension table so adding new icons in other formats (png, webp)
no longer requires touching getIconSrc.

* fix(dashboard): avoid nested button HTML in flag and funnel rows

List.Row renders via asChild, so wrapping cell contents in another
<button> produced nested interactive elements (flag row wraps
FlagActions, funnel row wraps a DropdownMenuTrigger). Swap the
inner element for a div with role="button" and keyboard handlers,
with biome-ignore because the lint suggestion would reintroduce
the nesting.

* fix(dashboard): guard Iridescence when WebGL is unavailable

`new Renderer()` from ogl throws synchronously when WebGL is
unsupported. Wrap construction in try/catch so the component
silently no-ops instead of crashing the whole subtree, and drop
the dead console.error after the guard.

* chore: tighten stale code comments

Trim the now-redundant comments explaining skip-on-org-level custom
events (the behavior is obvious from the flag) and the FIELD_RADIUS
constant in the OSS form.

* chore(turbo): rename AI_API_KEY to AI_GATEWAY_API_KEY in globalEnv

Aligns with the canonical env var read by apps/api/src/ai/config/models.ts.

* refactor(tracker): unify batch queues and drop unused engagement/bot helpers

- Collapse batch/vitals/errors/track flush logic into a single _meta-driven
  _enqueue/_flushQueue pair; public flushBatch/flushVitals/flushErrors/flushTrack
  remain as thin wrappers for callers and tests.
- Drop the engagement-time tracker, bot-detection setup, dynamic header
  resolution, and the unused TrackEvent type and toCamelCase helper — none of
  this is read anywhere downstream.
- Cleanup loop in destroy() iterates _meta to clear pending timers.

* refactor(evals): drop obsolete model field from EvalCase

The agent route no longer accepts a per-request model selector — there's a
single canonical agent now — so the runner should stop sending it.

* feat(db): add agent_chats table for persisted Databunny conversations

Stores the full UIMessage[] array as JSONB plus title/website/user/org FKs.
Indexes cover the sidebar list query (per-website + global per-user, both
ordered by updatedAt DESC).

* feat(rpc): add agentChats router for persisted Databunny chats

Exposes list/get/rename/delete keyed by user + website with workspace
permission checks, plus suggestedPrompts which seeds context-aware
suggestions from recent analytics insights and falls back to a static
set when none exist.

* refactor(mcp): slim payloads, harden inputs, improve error UX

- get_schema is now sectionable (events/errors/vitals/outgoing) with toggles
  for guidelines/examples; returns size + active sections so the LLM can pick
  a smaller payload.
- capabilities is filterable: 'include' selects sections, 'category'/'contains'
  filter queryTypes, and the heavy queryTypes block is now opt-in by default.
- list_insights gains 'ids' (direct drill-down via fetchInsightsForOrgs) and
  'fields' (slim row projection). insights-query.ts learns the ids filter.
- defineMcpTool wraps inputs with coerceMcpInput so clients that stringify
  every arg get auto-coerced booleans/JSON; ANSI codes are stripped from
  error messages so logs stay readable. Factory shape is now { build, toolName }
  to make missing toolNames a hard module-load error.
- ask now fails fast with a clear hint when AI_GATEWAY_API_KEY is missing,
  and rewrites upstream gateway 401s to a tool-level internal error.

* feat(api): persist agent chats with titles, followups, and rate limits

- Drop the user-facing model tier (basic/agent/agent-max) — there is one
  canonical analytics agent now.
- onFinish writes the full UIMessage[] to agent_chats (upsert by chatId).
  After persistence, generates 3 followup suggestions and patches them onto
  the latest assistant message metadata, then runs a first-turn title polish
  through the triage model. Both LLM calls are best-effort and never block
  the upsert.
- consumeStream() ensures onFinish runs even if the client disconnects.
- Add a 40-msg/10-min per-user rate limit on /v1/agent/dashboard/chat to
  guard against LLM-cost abuse, with 429 + retry headers.

* feat(dashboard): server-backed Databunny chat persistence

- ChatProvider now restores prior messages from agentChats.get and lets the
  agent route's onFinish handler write back to the server. Sidebar refetches
  on stream completion so new chats and polished titles land immediately.
- chat-history switches to the orpc agentChats router for list/rename/delete
  with optimistic updates and a SuggestedPrompts panel seeded from
  agentChats.suggestedPrompts.
- Drop the localStorage chat layer (use-chat-db, agent-chat-context,
  use-chat-status) and the standalone agent-header/title/navigation files —
  the page-content component now owns layout directly.
- Tighten agent-input/messages/commands around the new transport and surface
  followup suggestions from assistant message metadata.

* chore(agent): drop follow-up suggestions + post-stream sync

Remove generateFollowups, attachFollowups, AgentMessageMetadata, and the
secondary "update with followups" db write from the agent stream onFinish
handler. Drop the matching client-side post-stream sync timer and
isAwaitingSync state from the chat context — followups were the only
consumer of that data path. The chat list query is still invalidated on
stream finish so the sidebar picks up the polished title.

* chore(agent): delete dead slash command menu

The /command palette routed all 13 entries through the same plain text
sendMessage path — there was no toolChoice wiring or structural difference
from typing the prompt directly. Drop the menu, command list, hook, and
the atoms that backed them. Keeps agent-atoms.ts down to a single input
atom.

* refactor(agent): flatten input + inline pending queue

Drop the slash command menu plumbing and the heavy ai-elements Queue
compound from the input. Replace the Queue with a single inline
PendingPill row showing count + truncated preview + remove + clear-all.
KeyboardHints loses the dead "/" entry. Net: ~70 lines removed and one
dead AI Elements import gone.

* feat(agent): inspectable tool steps + active tool label + unified errors

- Tool steps are now click-to-expand. The expanded panel renders the
  formatted input parameters as a key/value list and the result as a
  small table (object arrays), key/value list (single object), pre block
  (primitive arrays), or single line (primitive). Caps preview at 5 rows
  / 5 columns / 120 chars per value.
- Replace the generic "Thinking" tail indicator label with the active
  tool name when one is in flight. The findActiveToolLabel helper walks
  the last assistant message in reverse for the most recent tool part
  without an output.
- Drop the inline-inside-assistant error branch and the synthetic
  errorAfterUser block. Errors now render through a single
  <Message from="assistant"> after the messages map regardless of whose
  role the last message had.
- AssistantActions now base at opacity-60 (was opacity-0) so copy and
  regenerate are discoverable on first render, snapping to opacity-100
  on hover/focus.

* feat(agent): welcome state cleanup + header website context

- Header now shows the website domain + favicon next to the agent name
  via FaviconImage and useWebsite. Grounds the conversation in which
  site it's about.
- Drop the four "Deep analysis / Pattern detection / Anomaly alerts /
  Auto reports" capability chips — they overlapped semantically with
  the suggested-prompt cards directly below. Single semantic surface
  for prompt launching now.
- Replace the generic "Databunny explores your analytics, uncovers
  patterns…" body copy with a single contextual subtitle:
  "Ask anything about <domain>'s analytics."
- Pass the domain through to WelcomeState so the empty state knows
  which site it's grounding.

* fix: icon

* fix(dashboard): drop text-foreground on empty state chart icons

* fix(dashboard): drop text-accent-foreground on funnels page icons

* fix(security): close CodeQL high-severity findings

- basket: loop HTML tag strip in sanitizeString to defeat stacked-tag
  bypass (js/incomplete-multi-character-sanitization, alert #43)
- tracker: replace Math.random() fallback in generateUUIDv4 with
  crypto.getRandomValues() so downstream IDs are cryptographically
  random (js/insecure-randomness, alerts #59 and #60)

* fix(tracker): outgoing-links plugin posted to nonexistent /outgoing route

The outgoing-links plugin has been POSTing to `/outgoing` since 2025-12-27.
basket has no such route — it serves outgoing-link events at `POST /` with
`type: "outgoing_link"` in the body. Every external link click was 404'ing
silently in production.

Confirmed via direct ClickHouse query: zero rows in `analytics.outgoing_links`
for any site that has `trackOutgoingLinks: true` enabled.

Switch the plugin to:
- POST `/` with `type: "outgoing_link"` (the route basket actually serves)
- Send `client_id` as a query param so beacon transport works
  (sendBeacon strips custom headers, including `databuddy-client-id`)
- Include `anonymousId`, `sessionId`, `timestamp` so clicks are attributed
  to a session — basket's insertOutgoingLink reads these but the old
  payload never sent them, so all click rows would have been anonymous
  even if the route had worked

Verified end-to-end against prod basket: probe POST returned 200 success
and the row landed in `analytics.outgoing_links` with the correct
href/text/session_id (the first row ever for a real customer site).

* test(tracker): strict basket route allowlist + auto-fixture

Why this change:
The /outgoing tracker bug fixed in 918d1967 hid in green E2E tests for
3+ months because every spec used the same blanket mock pattern:

  await page.route("**/basket.databuddy.cc/*", async (route) => {
    await route.fulfill({ status: 200, ... });
  });

A catch-all that returns 200 for any path. Assertions only checked
tracker-side behaviour (`req.url().includes("/outgoing")`), so the
contract between tracker and basket was never tested.

What this adds:
- BASKET_ROUTES allowlist in tests/test-utils.ts mirroring the routes
  basket actually serves (apps/basket/src/routes/{basket,track,llm}.ts)
- A custom Playwright `test` exported from test-utils with an `auto: true`
  fixture that:
    1. Tracks every basket request via `page.on("request", ...)` so the
       observer fires before any test mock can intercept and shadow it
    2. Default-fulfills known routes 200, unknown routes 404
    3. Throws at teardown if any unknown route was hit, with a clear
       message pointing at BASKET_ROUTES so the next dev knows where
       to update if a new route is genuinely added

Migrating the tests:
- All 16 spec files now `import { test } from "./test-utils"` instead of
  `@playwright/test` — auto-fixtures run for every test, no opt-in
- Deleted ~15 redundant `**/basket.databuddy.cc/*` catch-all beforeEach
  blocks (the fixture handles them)
- Refactored one test in audit-bugs.spec.ts that was using `page.route`
  as a request observer — now uses `page.on("request")` so it doesn't
  shadow the fixture's tracking
- Updated outgoing-links spec predicates from `req.url().includes("/outgoing")`
  to `e.type === "outgoing_link"` (matches the new payload from the plugin
  fix and is the body-shape pattern the hardened mocks expect)
- Marked edge-cases pixel-mode test as `test.fixme()` — the hardening
  immediately uncovered a separate real bug in pixel.ts (it routes
  `/batch`/`/track`/`/vitals`/`/errors` as GET image loads to paths basket
  only serves as POST). Tracked separately, fix is out of scope here.

Verification:
- 126 passed, 6 skipped, 0 failed on chromium
- Temporarily reverted the outgoing-links fix and re-ran the spec: 6 of
  13 outgoing-links tests fail with the exact "Tracker hit unknown basket
  route(s): POST /outgoing" message — proving the regression guard catches
  the bug class it's meant to catch

* fix(tracker): pixel plugin routes all events to /px.jpg

basket only serves pixel transport at GET /px.jpg — there is no
GET /batch, /track, /vitals, /errors. The pixel plugin only translated
the `/` endpoint to `/px.jpg` and left every other endpoint alone, so
batched screen views, custom track events, vitals and errors all fired
GET image loads to dead routes in pixel mode (`usePixel: true`). The
new strict basket route allowlist in tests/test-utils.ts caught this
the moment the hardening landed.

basket's parsePixelQuery (apps/basket/src/utils/pixel.ts) dispatches on
a `type` query param and only handles `track` / `outgoing_link`. So:

- Always route to `/px.jpg`, never the original endpoint
- Add a `pixelEventTypeFor()` mapping that translates `/`, `/batch`,
  `/track` → `track` and `/outgoing` → `outgoing_link`
- /vitals and /errors return null and silently no-op — they have no
  pixel equivalent and the pre-existing GET /vitals, GET /errors
  behaviour was already broken in production
- Set `type` as a query param so basket's parsePixelQuery dispatches
  correctly (overridable by an explicit `type` field on the data)
- Split batched arrays into one pixel call per event since /px.jpg
  only accepts a single event per GET. Pre-existing behaviour silently
  flattened the array indices into garbage params like `0[name]=...`

Updated the pixel test to assert the request actually lands at /px.jpg
(not just "any GET"), which would have caught this bug if it had been
written that way originally.

Verified: 127 passed, 5 skipped (WebKit only), 0 failed. The pixel test
that was test.fixme'd in the previous commit now passes.

* chore(tracker): desloppify test-utils + pixel plugin

Net -112 LOC across 3 files (101 added, 213 removed).

test-utils.ts (259 → 170):
- Inline setupBasketMock into the auto-fixture; nothing imports it
  externally so the indirection added zero value
- Inline isKnownBasketRoute; replace the regex array with a Set<string>
  keyed on `${method} ${path}` — the routes are exact matches, regex
  was overkill
- Drop UnknownBasketRouteHit and BasketMock interfaces; use inline
  type literals (also unused externally)
- Hoist CORS_HEADERS to module scope, it's static
- Drop the try/catch around `new URL(req.url())` — Playwright requests
  always have valid URLs
- Collapse the duplicate 200/404 fulfill blocks into a single call
- Strip JSDoc walls that just restated function signatures

pixel.ts (143 → 121):
- Replace pixelEventTypeFor() with a PIXEL_TYPE_BY_ENDPOINT lookup
  table — three branches into a four-line const
- Hoist flatten() out of the closure into a module-level
  flattenIntoParams(); doesn't depend on any closure state
- Delete the dead `prefix === "" && key === "properties"` branch — it
  called the same code path as the else branch (when prefix is "",
  newKey === key, so both branches were identical)
- Strip the wall comment explaining the endpoint mapping; the const
  table speaks for itself

edge-cases.spec.ts:
- Drop the wall comment on the pixel test assertion

Verified: 127 passed, 5 skipped, 0 failed. tsc + ultracite clean.

* chore(api): add tokenlens for agent token + cost telemetry

Pulls in tokenlens@1.3.1 — a small registry of LLM model metadata
(context windows + per-token pricing) for the Vercel AI Gateway
catalog. Used by the upcoming agent route telemetry to compute
USD cost per chat turn from result.totalUsage.

* feat(api): token + cost telemetry on agent stream

Add a small summarizeAgentUsage helper that reads result.totalUsage
from the agent stream and emits raw token counts (input, output,
cache read/write, reasoning) plus best-effort USD cost via the
tokenlens Vercel AI Gateway catalog. Falls back to anthropic/claude-4-sonnet
when the exact gateway model id isn't in the catalog yet — directionally
correct, with cost_fallback flagged so analytics can correct estimated
rows later.

The agent route fires the telemetry as a parallel side effect after
result.consumeStream() — the totalUsage promise resolves once the stream
finishes, so awaiting it never blocks the response. Output goes to:
- mergeWideEvent (evlog wide-event coverage rule)
- trackAgentEvent("agent_activity", { action: "chat_usage", ... })

Failures are captured via captureError and never break the chat flow.

Also export modelNames from ai/config/models so the telemetry helper
can resolve the canonical id without re-declaring it.

* chore(agent): drop redundant + outdated comments

- usePendingQueue / useChatLoading: remove JSDoc that just restated
  the function signature.
- useChatLoading JSDoc referenced "post-stream metadata sync" which was
  removed alongside the followup suggestions earlier this session —
  drop it.
- Trim the verbose 4-line "Token + cost telemetry. Fire-and-forget side
  effect..." comment in agent.ts to a single sentence; the helper name
  and Promise.resolve pattern carry the meaning already.

* feat(billing): expand autumn config — uptime, status pages, alarms, gated features

Adds full feature coverage to autumn so every billable resource is declared
in one place. Server-side enforcement is wired up in follow-up tickets;
this commit is config-only.

New features:
- monitors  — already declared, now used in main plans (Free 1 / Hobby 5 /
  Pro 25 / Scale 50) plus expanded Pulse counts (Pulse Hobby 25 → was 10,
  Pulse Pro 150 → was 50). Cost driver is checks not monitors, so frequency
  is gated separately.
- uptime_minute_checks (boolean) — gates sub-5-minute granularity. Pro+ on
  main plans, all Pulse plans. Free/Hobby capped at 10-min granularity.
- status_pages (metered) — count cap. Hobby 1 / Pro 3 / Scale 5 / Pulse
  Hobby 3 / Pulse Pro 10.
- status_page_custom_branding (boolean) — paid feature, was free for all.
- status_page_custom_domain (boolean) — Pulse Pro only.
- alarms (metered) — count cap. Hobby 5 / Pro 50 / Scale unlimited /
  Pulse Hobby 25 / Pulse Pro unlimited.
- webhook_alert_destinations (boolean) — Pro+ and all Pulse plans. Free
  and Hobby get email-only.
- funnels, goals, feature_flags, target_groups (metered) — moved from
  shared/features.ts hardcoded limits into autumn so billing is the
  single source of truth.
- retention_analytics, error_tracking (boolean) — Hobby+, was previously
  only enforced in shared/features.ts.

Existing features wired up properly:
- seats — was declared but never attached to any plan. Now Free 2 /
  Hobby 5 / Pro 25 / Scale unlimited.
- rbac — was declared but never attached. Now Pro+ and Scale.
- sso — sso_plan addon now actually has the boolean item.

Plan structure preserved:
- Free $0, Hobby $9.99, Pro $49.99, Scale $99.99 (phasing out — no
  expansion), Pulse Hobby $14.99, Pulse Pro $49.99, SSO addon $100.
- All existing event tier overage pricing kept identical.
- Agent credits + rollover on Pro unchanged from prior commit.

* feat(api): enforce agent credits via autumn

Wire the agent route to autumn for credit enforcement now that the
config is pushed.

- Resolve billing customer id via getBillingCustomerId once user auth
  succeeds. Skipped for API-key flows (no clear billing owner).
- Pre-stream check on agent_credits returns 402 OUT_OF_CREDITS if the
  customer has no remaining balance and no overage allowance.
- Post-stream telemetry side effect now also fires autumn.track for the
  4 metered token features (input/output/cache_read/cache_write). Autumn
  auto-deducts credits via the credit_system creditSchema mapping.
- Track failures captured via captureError, never block the chat flow
  (Promise.allSettled around the 4 tracks).
- Zero-value tracks filtered out so we don't pollute autumn with no-op
  events.

* chore(billing): remove seats — team members unlimited on all plans

Drops the seats metered feature entirely from autumn.config.ts (was
Free 2 / Hobby 5 / Pro 25 / Scale unlimited) and removes all seats
items from every plan.

Also sets PLAN_FEATURE_LIMITS.TEAM_ROLES to "unlimited" across all
tiers in packages/shared/src/types/features.ts so the dashboard UI
stops gating team size. Upgrade message updated to "Team members are
unlimited on all plans".

Seat-based pricing is off the table for Databuddy.

* feat(agent): surface credit balance in header

Add an AgentCreditBalance pill next to the chat history and new-chat
buttons in the agent header. Uses the existing useUsageFeature hook
from billing-provider (which proxies autumn's useCustomer). Shows:

- "234 / 5,000" format with tabular-nums
- Amber warning state at <20% remaining
- Destructive state at 0 remaining
- ∞ when the plan grants unlimited (Scale, comped orgs)
- Click → /billing
- Skeleton during first load
- Auto-refetches 1.5s after stream finish to pick up the post-turn
  autumn.track decrement

* fix(uptime): align heatmap day buckets with user timezone

Latest day appeared empty on pulse and status pages for users west of
UTC because the client re-parsed backend UTC date strings through
localDayjs, shifting them back a day. Uptime time-series queries also
bucketed in server tz instead of user tz.

* chore(api/mcp): coerce inputs via z.preprocess

Compose `coerceMcpInput` into the schema once with `z.preprocess`
instead of calling it inline at parse time. Drop redundant explanatory
comments now that the wiring is the obvious shape.

* chore(dashboard): add atmn cli, ignore generated sdk d.ts

Pull in `atmn` so we can `atmn push` autumn config from this repo.
The companion `@useautumn-sdk.d.ts` file is regenerated on every
`atmn pull` and would otherwise drift constantly — gitignore it.

* chore(billing): rescale agent credits 5x cheaper, 5x budgets

Old schema: 1 credit ≈ \$0.005 of LLM compute. Free tier of 100 credits
covered ~13 first-turn chats — felt stingy. New schema: 1 credit ≈ \$0.001,
free 500, hobby 2.5k, pro 25k (with rollover + rescaled overage tiers).
Same USD margin per plan, ~5x more perceived value.

Also fold the comment blocks into /* */ syntax — atmn pull was
shredding consecutive // lines into single-line paragraphs separated
by blank lines, and biome was happy to keep that mess.

* feat(agent): cost-aware overhaul + thinking effort toggle

Telemetry: bill fresh input tokens, not the full input count. Cache
read/write were tracked as their own metered features, so the prior
code was charging cached tokens twice (once at the input rate, once
at the cache rate). UsageTelemetry now exposes fresh_input_tokens
from inputTokenDetails.noCacheTokens and the route bills that.

Tools: delete redundant tools that get_data already covers
(execute_query_builder, get_top_pages, get_link, search_links,
get_funnel/goal/annotation_by_id) and trim every remaining tool's
description + parameter describe() calls. The 151-builder list dump
moves out of the schema and into a discoverable error on unknown type.

Prompts: drop CLICKHOUSE_SCHEMA_DOCS from the always-on system prompt
and inline a minimal table hint into execute_sql_query (the only tool
that needs schema knowledge). Condense analytics rules, examples, and
chart guide to the essentials.

Dead code: triage and reflection agents were never invoked from
production — only createAgentConfig("analytics", ...) is called.
Delete the agent files, their prompts, the tool files only they
imported, and collapse createAgentConfig to a passthrough.

Result on a "hi" baseline turn: cache_write 17,322 → 7,683 tokens
(-56%), credits 13.27 → 6.38 (-52%) under the old schema. Stacked
with the credit rescale, free tier delivers ~200 turns vs ~13.

Thinking: user-selectable extended thinking via a new compact control
in the agent input footer. AgentThinking = "off" | "low" | "medium"
| "high" flows from a jotai atom (atomWithStorage so it persists)
through the chat transport into the route body, into AgentContext,
and into Anthropic's thinking.budgetTokens via providerOptions. The
route now drops temperature when thinking is enabled because Anthropic
rejects the combination. createToolLoopAgent actually threads
providerOptions through now — it was dead code before.

Layout fix: agent input footer was shifting on every submit because
KeyboardHints returned null while loading and the Stop button got
inserted between the Thinking control and Send. Hints now swap to
"Generating…" in the same slot, and Send/Stop share one slot that
toggles by state. Footer is pixel-stable across both states.

Probe harness: new apps/api/scripts/agent-cost-probe.ts that runs the
real agent pipeline (same model, same tools, same providerOptions),
supports multi-turn chats and --thinking=off|low|medium|high, and
prints token + credit breakdowns under both schemas.

Cleanup: drop the double cast on apiKey.organizationId, drop the as
LanguageModel cast on models.analytics, drop unused textareaRef in
agent-input, fold get-data's truncation into the mapping pass, remove
a leftover formatLinkForDisplay helper that only existed for the
deleted search_links output.

* revert(billing): restore original per-token credit cost

Back to 1 credit ≈ \$0.005 of compute (input 0.000_6, output 0.003,
cache read 0.000_06, cache write 0.000_75). Plan budgets stay at the
bigger 500/2500/25k numbers — users still get meaningful headroom,
we just no longer subsidize the per-token math on top of that.

Probe now prints runway against the three real plan sizes (free,
hobby, pro) instead of comparing against a theoretical proposed
schema.

* fix(dashboard): gate nav item flag filter on hydration

Items and sections inside the sidebar nav memo were filtered by
getFlag() directly, with no isHydrated guard. The FlagsProvider reads
from localStorage synchronously on the first client render, so the
flag store was populated on the client but empty on the server. Any
flag-gated nav item (e.g. Home > Insights) was rendered on the client
and absent on the server, producing React error #418 hydration
mismatches on every page sharing the (main) layout: /websites,
/onboarding, /demo/*, /status/*, and nested website routes.

Mirror the existing filterCategoriesByFlags pattern: treat all flags
as off until isHydrated is true. Server and first client paint now
agree, and flag-gated items appear on the next render once hydration
completes.

* polish(dashboard/agent): dropdown thinking control, slimmer header

Thinking picker moves from Popover + button grid to DropdownMenu
+ DropdownMenuRadioGroup so keyboard nav + selected state come from
the primitive. Header drops the inline favicon/domain and adds a
thin separator before the right-side action cluster.

* Update page.tsx

---------

Co-authored-by: Chandra-Sekhar <sekhargandrotula@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants