From 60e13dbf6f560fc954fad1cdc0fb7732e009ebfe Mon Sep 17 00:00:00 2001
From: iza <59828082+izadoesdev@users.noreply.github.com>
Date: Fri, 31 Jul 2026 16:39:49 +0300
Subject: [PATCH 001/131] feat(links): add deep link redirects
---
.env.example | 2 +
.../dashboard/app/(dby)/dby/l/[slug]/page.tsx | 112 +++--
apps/dashboard/app/(dby)/dby/og/route.tsx | 5 +-
.../links/_components/deep-link-icons.tsx | 19 +-
.../links/_components/deep-link-sheet.tsx | 66 +--
.../links/_components/link-constants.ts | 6 +-
.../links/_components/link-form-schema.ts | 22 +
.../(main)/links/_components/link-item.tsx | 4 +-
.../(main)/links/_components/link-qr-code.tsx | 4 +-
.../(main)/links/_components/link-sheet.tsx | 4 +-
.../(main)/links/_components/link-utils.ts | 14 +-
.../links/_components/links-search-bar.tsx | 64 ++-
.../links/_components/use-og-metadata.ts | 5 +-
apps/dashboard/app/(main)/links/page.tsx | 55 +--
apps/dashboard/app/api/auth/[...all]/route.ts | 2 +-
apps/dashboard/app/api/image-proxy/route.ts | 16 +-
.../ai-components/renderers/links/list.tsx | 9 +-
.../ai-components/renderers/links/preview.tsx | 3 +-
apps/dashboard/lib/ai-components/schemas.ts | 8 +-
apps/dashboard/lib/links-url.ts | 26 ++
apps/links/src/index.ts | 6 +-
apps/links/src/lib/deep-link-fallback.ts | 78 ++++
apps/links/src/lib/producer.ts | 208 ++++-----
apps/links/src/routes/redirect.ts | 312 +++++++------
apps/links/src/utils/geo.ts | 26 +-
dashboard.Dockerfile | 2 +
docker-compose.selfhost.yml | 2 +-
infra/railway-template.md | 9 +-
packages/ai/src/ai/mcp/tools.ts | 58 ++-
packages/ai/src/ai/tools/link-catalog.ts | 3 +
packages/ai/src/ai/tools/links.ts | 93 ++--
packages/ai/src/query/builders/links.ts | 19 +-
packages/env/src/app.ts | 7 +
packages/env/src/public.ts | 1 +
packages/redis/links-cache.ts | 328 +++++++++++--
packages/rpc/src/routers/link-access.ts | 27 ++
packages/rpc/src/routers/link-folders.ts | 25 +-
packages/rpc/src/routers/links.schemas.ts | 31 +-
packages/rpc/src/routers/links.ts | 438 ++++++++++++++----
packages/shared/package.json | 2 +
.../shared/src/constants/deep-link-apps.ts | 81 ++--
packages/shared/src/constants/links.ts | 15 +
.../shared/src/utils}/trusted-client-ip.ts | 12 +-
packages/validation/package.json | 1 +
packages/validation/src/schemas/index.ts | 1 +
packages/validation/src/schemas/urls.ts | 6 +
turbo.json | 1 +
47 files changed, 1542 insertions(+), 696 deletions(-)
create mode 100644 apps/dashboard/lib/links-url.ts
create mode 100644 apps/links/src/lib/deep-link-fallback.ts
create mode 100644 packages/rpc/src/routers/link-access.ts
create mode 100644 packages/shared/src/constants/links.ts
rename {apps/dashboard/lib => packages/shared/src/utils}/trusted-client-ip.ts (70%)
create mode 100644 packages/validation/src/schemas/urls.ts
diff --git a/.env.example b/.env.example
index 44cee2c2f7..b9dc1a81ff 100644
--- a/.env.example
+++ b/.env.example
@@ -17,11 +17,13 @@ REDIS_PASSWORD=""
DASHBOARD_URL=""
API_URL=""
BASKET_URL=""
+LINKS_URL=""
# Baked into the dashboard browser bundle. Set these before production builds.
NEXT_PUBLIC_APP_URL=""
NEXT_PUBLIC_API_URL=""
NEXT_PUBLIC_BASKET_URL=""
+NEXT_PUBLIC_LINKS_URL=""
NEXT_PUBLIC_STATUS_URL=""
AI_GATEWAY_API_KEY=""
diff --git a/apps/dashboard/app/(dby)/dby/l/[slug]/page.tsx b/apps/dashboard/app/(dby)/dby/l/[slug]/page.tsx
index 3a606ff961..4f883c04d2 100644
--- a/apps/dashboard/app/(dby)/dby/l/[slug]/page.tsx
+++ b/apps/dashboard/app/(dby)/dby/l/[slug]/page.tsx
@@ -2,50 +2,79 @@ import { db } from "@databuddy/db";
import {
type CachedLink,
getCachedLink,
- setCachedLink,
- setCachedLinkNotFound,
+ ratelimit,
+ setCachedLinkIfAbsent,
+ setCachedLinkNotFoundIfAbsent,
} from "@databuddy/redis";
+import { getTrustedClientIp } from "@databuddy/shared/utils/trusted-client-ip";
import type { Metadata } from "next";
+import { headers } from "next/headers";
import { notFound, redirect } from "next/navigation";
+import { cache } from "react";
import { APP_URL } from "@/lib/app-url";
+import { getSafeHttpUrl, isPublicLinkSlug } from "@/lib/links-url";
-async function getLinkBySlug(slug: string): Promise
Recommended
- {outcome.recommendation.action} + {recommendation.action}
- {item.entity.type === "goal" && outcome.recommendation.operation ? ( + {isInstrumentationRecommendation(recommendation) ? ( +{access.reason}
+ ) : null} +{insight.summary}
- {insight.recommendation ? ( + {recommendation ? (Next step - {insight.recommendation.action} + {recommendation.action}
- {insight.signal.entity.type === "goal" && - insight.recommendation.operation ? ( + {isInstrumentationRecommendation(recommendation) ? ( +
{goal.name}
diff --git a/apps/dashboard/test/e2e/specs/regressions/measurement-recommendations.spec.ts b/apps/dashboard/test/e2e/specs/regressions/measurement-recommendations.spec.ts
new file mode 100644
index 0000000000..74124af7fd
--- /dev/null
+++ b/apps/dashboard/test/e2e/specs/regressions/measurement-recommendations.spec.ts
@@ -0,0 +1,118 @@
+import { randomUUID } from "node:crypto";
+import { db } from "@databuddy/db";
+import { analyticsInsights, insightObservations } from "@databuddy/db/schema";
+import type {
+ InvestigationOutcome,
+ InvestigationSignal,
+} from "@databuddy/shared/insights";
+import { expect, test } from "@/test/e2e/fixtures";
+
+test(
+ "opens an editable goal draft from an insight recommendation",
+ { tag: ["@regression"] },
+ async ({ authenticatedPage, e2eSession }) => {
+ expect(e2eSession.websiteId).toBeTruthy();
+ if (!e2eSession.websiteId) {
+ throw new Error("Expected the E2E session to include a website");
+ }
+
+ const insightId = randomUUID();
+ const signalKey = "measurement:conversion-coverage";
+ const createdAt = new Date();
+ const signal: InvestigationSignal = {
+ signalKey,
+ entity: {
+ type: "website",
+ id: e2eSession.websiteId,
+ label: "E2E Website",
+ },
+ metric: {
+ label: "Conversion measurement coverage",
+ current: 0,
+ format: "number",
+ },
+ changePercent: null,
+ severity: "info",
+ sentiment: "neutral",
+ period: {
+ current: { from: "2026-07-25", to: "2026-07-31" },
+ previous: { from: "2026-07-18", to: "2026-07-24" },
+ },
+ };
+ const outcome: InvestigationOutcome = {
+ title: "Checkout completion needs a measurable goal",
+ summary:
+ "The site has active traffic but no configured conversion measurement.",
+ impact:
+ "The team cannot see whether people complete checkout from the current analytics setup.",
+ rootCause: null,
+ evidence: [
+ "The completed period recorded 68 sessions and 142 pageviews without an active goal or funnel.",
+ ],
+ publish: true,
+ recommendation: {
+ kind: "goal_draft",
+ action: "Review a goal for completed checkout.",
+ draft: {
+ name: "Checkout completed",
+ description: "Measure completed checkout events.",
+ type: "EVENT",
+ target: "checkout_completed",
+ filters: [],
+ ignoreHistoricData: false,
+ },
+ },
+ next: {
+ type: "resolve",
+ reason: "This proposed goal is ready for teammate review.",
+ },
+ };
+
+ await db.insert(analyticsInsights).values({
+ id: insightId,
+ organizationId: e2eSession.organizationId,
+ websiteId: e2eSession.websiteId,
+ title: outcome.title,
+ description: outcome.summary,
+ severity: "info",
+ sentiment: "neutral",
+ changePercent: null,
+ dedupeKey: `${e2eSession.websiteId}|${signalKey}`,
+ subjectKey: signalKey,
+ timezone: "UTC",
+ status: "resolved",
+ createdAt,
+ });
+ await db.insert(insightObservations).values({
+ id: randomUUID(),
+ organizationId: e2eSession.organizationId,
+ websiteId: e2eSession.websiteId,
+ insightId,
+ signalKey,
+ asOf: createdAt,
+ signal,
+ evidence: outcome.evidence,
+ outcome,
+ recheckAt: createdAt,
+ createdAt,
+ });
+
+ await authenticatedPage.goto("/insights");
+
+ await expect(
+ authenticatedPage.getByText(outcome.title, { exact: true })
+ ).toBeVisible();
+ await authenticatedPage
+ .getByRole("button", { name: "Review goal draft" })
+ .click();
+ await expect(
+ authenticatedPage.getByText("Review Goal Draft", { exact: true })
+ ).toBeVisible();
+ await expect(
+ authenticatedPage.getByDisplayValue("Checkout completed")
+ ).toBeVisible();
+ await expect(
+ authenticatedPage.getByDisplayValue("checkout_completed")
+ ).toBeVisible();
+ }
+);
diff --git a/apps/insights/src/agent.ts b/apps/insights/src/agent.ts
index 04f0a3e808..02e01d0173 100644
--- a/apps/insights/src/agent.ts
+++ b/apps/insights/src/agent.ts
@@ -7,8 +7,12 @@ import {
import { getAILogger } from "@databuddy/ai/lib/ai-logger";
import {
agentInvestigationOutcomeSchema,
+ investigationOutcomeSchema,
+ type AgentInvestigationOutcome,
type InvestigationOutcome,
type InvestigationSignal,
+ type InsightMeasurementRecommendation,
+ type InsightWatchThreshold,
} from "@databuddy/shared/insights";
import {
type LanguageModel,
@@ -19,6 +23,7 @@ import {
type ToolSet,
ToolLoopAgent,
} from "ai";
+import type { MeasurementCandidate } from "./detection";
const MAX_STEPS = 8;
const TIMEOUT_MS = 2 * 60_000;
@@ -49,6 +54,7 @@ export interface InsightAgentInput {
kind: "reply";
}
)[];
+ measurementCandidate?: MeasurementCandidate;
otherOpenWork: {
asOf: string;
next: InterruptingNext;
@@ -90,7 +96,9 @@ Return one next outcome:
Act and ask interrupt people. Use either only when the result is worth interrupting a teammate now. A missing description or unclear name alone is not an alert.
When an action changes the named goal's title or description, set next.execution to the exact goal edit so Databuddy can apply it transactionally on click. When an action removes a duplicated or useless named goal, set next.execution to the exact delete. Omit execution for code, tracking, external, or any other action that Databuddy cannot safely apply itself. Never provide an execution for a different entity.
For every act or watch, set next.recheckAt to the earliest exact ISO 8601 time after asOf when its verification or escalation condition can be measured. Use the actual measurement window or sample window, not a generic tomorrow. Never schedule a recheck before the window can answer the condition; when no defensible time exists, resolve or ask instead.
+For every evidence item, return one evidenceRefs item in the same order. Use source=provided with the zero-based supplied-evidence index for supplied facts, or source=tool with the exact name of a read tool you used. Never cite a tool you did not use. For every watch, return next.threshold with the exact native-unit value, comparison, defensible anchor, and evidenceRef. The system writes the customer-facing escalation sentence from this structured condition.
A recommendation is one concrete, non-interrupting next step on a published insight; otherwise use null. Name the exact object and evidence-backed change, never generic narrowing or an invented target. Code, hosting, browser, or integration recommendations require inspected source or configuration; an error message, stack, route, or common implementation pattern is not enough. If source access is the next move, use ask and recommendation null rather than proposing a speculative repair. Goal edits put the proposed name and business description in changes, with null for an unchanged field, and action names the proposed value. Goal deletes and non-goal recommendations use null changes. operation is null unless the exact goal editor action is edit or delete. Never combine a recommendation with act or ask, confuse an event with a goal, or claim a proposal was applied, fixed, or verified.
+When supplied or inspected evidence establishes an exact measurement candidate, you may return a typed goal_draft or funnel_draft recommendation. measurementCandidate is a backend-verified candidate: copy its target exactly, and never turn a page_navigation_proxy into a goal or funnel draft. Copy only the exact PAGE_VIEW path or EVENT name that evidence establishes; never infer a target, invent an event, use CUSTOM, add conditions, or widen the 24-hour funnel window. A goal draft has one target; a funnel draft has two to ten ordered steps. These drafts are review-only: set next to resolve, omit next.execution, and explain that the teammate can edit the normal setup form before saving. Route-only evidence proves navigation, not a business conversion. Label a route-only funnel as a navigation proxy and prefer an instrumentation recommendation when the missing product event is the real limitation. An instrumentation recommendation is display-only, names the behavior that needs measurement, and must never claim a goal or funnel already exists.
Measured reliability or performance harm to a named cohort is impact even when revenue is unknown. A goal or funnel that contradicts its configured purpose or inspected source is broken tracking: act on the exact definition and verification, with no recommendation. Without a configured purpose, do not invent or ask for one. If an undescribed goal combines unrelated behaviors, explain what it measures, put the exact target and filters in rootCause, state what the number cannot tell the teammate in impact, and resolve because no isolated failure is proven. Recommend renaming and describing the broad goal, or creating a narrower goal from an existing purpose-specific event; delete only a duplicate or useless goal. Publish this limitation once. If its description already defines broad engagement, keep it and investigate the change.
An improvement from a failing value to another failing value is not recovery. For performance regressions, identify the worst meaningful route and affected traffic before deciding; if the metric remains unhealthy and code ownership is missing, ask for that ownership instead of inventing a fix or waiting on a noise-sensitive threshold. The same rule applies to ongoing reliability harm: when a current failure affects a material named cohort and repair needs source access, ask for the owning repository now; do not watch it merely because the exact code mechanism is not yet inspected.
An event name does not prove whether more or less is good. Never resolve an unexplained event change from its name alone; inspect its definition, emission code, related workflow, and revenue evidence. If its meaning remains unknown, do not open a case for ambiguity alone; ask only when an external fact gates an already-material fix.
@@ -129,6 +137,174 @@ function promptSignal(signal: InvestigationSignal) {
};
}
+const watchAnchorCopy: Record
{prompt.heading}
diff --git a/apps/dashboard/components/monitors/collapsible-section.tsx b/apps/dashboard/components/monitors/collapsible-section.tsx index 78769feaed..ebd16af857 100644 --- a/apps/dashboard/components/monitors/collapsible-section.tsx +++ b/apps/dashboard/components/monitors/collapsible-section.tsx @@ -3,6 +3,7 @@ import { AnimatePresence, motion } from "motion/react"; import { cn } from "@/lib/utils"; import { CaretDownIcon } from "@databuddy/ui/icons"; +import { Button } from "@databuddy/ui"; interface CollapsibleSectionProps { badge?: number; @@ -23,10 +24,11 @@ export function CollapsibleSection({ }: CollapsibleSectionProps) { return (
KLR#6!Buf4FcX7B9R^L#m%hDQp#p##L*9z
zcD+0V+EvKi!QYCBI_J??*&)ic?a5%2M9DJsH?0U}e`YVyIc@+ ht}t_Bad$DgfE&5v#U5dZN2kcR7`kp?=s9$Fu_7%6?qT3>-=p4u21{Dkn2O(h
zwB<}5Xa fw2x4r`VW}BIC4xw6|e4J*6!-ZQ>UM#
zYGln;Mj;UsXrj$h0S}ERYTA?}={nngNOxuS42Ym?k|5jfh2HfBOl^M0ttF~FPv84~
z{uCQIS0y+1Om>2w`A?`VD=$nfZl%$9S-wQ7>TRnUYbDt+rXc9L@f>-^i+9B_OxXUB
zV&yBVfelnr84io4K^5Du7_E`2g&GL)>W6P$UV*L;6hCmeR_
0JPCj~iiBCb$FlZNj$j{x=Lbrr>+O)HZwbcCA87VPw|_e~rca(j
z0rj+eRJBSK)x-hLrK>-^tieFcZQv)(*!ib)XUpx0SgjFbQw_Krh_ESB{hnDu`WLH9
z(-w`Fxz7b+*rl9bouLohqTR|+YAKr`iN}j(Tnj3Tr~YzYir|dzI{E#SvuVnP>7#w`
z)Qi-8{B+eV@B>gPLLeMXLbre?
zS`H4!ONMP)EneLpdlsmWPV#nULnJoNemvUqpMEgUNeuzOi4C+jTVe?jA(}P44HL4O
z^6--xU*$vHc4tjFdkodCS}*PPORTjZ%BmSf=9!er7MW&y6)xCFqFBj_TyNXO{xt#n
z8Pa?J^*LoZikhReHcfgl!B|_!^%ioQ#%cN1Y74-d=jNtU-9o1F>U*puDWOh3Ns0Dn
zBEwyBMHyh=-H$FEhr}SK1{Lzlpgqr`HQOG83Eo(&fVog1uzoQ5d5g*h1dA#6B7h`3
z8|hdSq^){um`(fexv#uCgcH$^RPAxYjRvx>o|5$-aPHfHFaY^<<39=l|Eyr!z8WAy
z>j8rQ6A}FlI2(t!udWqOcyjXiedUtoJYP8iD$$BN;`1*9izp-keOh^;?sQ()QaD{hU|aFe>ZBd2ug1B-!Y`F>
z8${=gOwzEo7RfcjI@&Zeze-;U>^nXzyo+c8d?p23uUb!dtat!RZ2V<0?Q8RHs51%x
zCXq2H_f7)$rt4TvUBokvO>TU?P^B
z4abc2U`5)E<-6asoV%ZE8W0fAy`S1;9#*h=_T@&g7W2G>Su8UvM=RHvnk{UaFM*>D
z+m(9Op#aN{XT@FwUr!*fRbV1~G6lVIP>8KlGedekqvZqA!FpBK
u#L
5*6FzIu+Gf$UNYhIF;=Bc9~vEQzuYSAO8JPl2`9irhW
zQA7M~Fe`kYYAM3a2nGPLFV-BssyOE5Qfj2I?Z=dQMl5eY3idfc>+7TTk`O90APr4`
zl3S&Rxp74$AW6hPk4CQT9X{eLI3-Sz&Mvx?!=+pMGc%<%N~}r+of2WByz0aIQlXK4
zksUcecnS{tRD3zh;{lb`H6IK_K@ZTqUE^}yhToVIlr}+YPUjFg77ws7krOe6n;&Kx
zpS?&fm`2r+d7Kj*_(>(2z~VJF6^qoFLC%c(7k^yQMX3d5M8F5j>Rmc#{wX|2zR^j1
zsV#O0*gc*6?&%DYAmw^zwy8-KmBMmfuyv?JNtC|=W^1*aa-_1d8Lc8I`
X@l7LzCB
zBOEFwr*5he$(RZ>`digYNr`^-zHyT)b%*<9&t`f*
zbn&Gt7rrjv<4!!a+orQ1EqtypWYLXrGtdSq?L>x%@YIoc&>ZMJv{Q$Y0HfG&ub*2!
z@!T%53nEc*m5#Hg8+Yi0A^B;=9o7P;y>WlwBW!~e0CdoxckX|@bHm!*%pP`zGT4oo
zIhCVQVd^RWGy3o`rsuKS10(jpp~oIATWXYWIW2^W_v5MLDZz|2%-L)l7q*EJ>6LdA
z5?Timm}LlZ8+`QRA*3FYW_dtDRp!