Skip to content

Commit 2c05461

Browse files
committed
docs: /business/ sends its events to the Devforth back office, the build instructions open a start link an agent fetches, so the funnel shows who copied them and who ran them
1 parent 6e59bd2 commit 2c05461

5 files changed

Lines changed: 70 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ through my agent; screens are for checking. Keep the first version small and
7070
ready for more parts later (orders, jobs, stock, invoices).
7171
7272
START HERE
73-
Read https://adminforth.dev/docs/tutorial/gettingStarted/ and
74-
https://adminforth.dev/llms.txt first. Use AdminForth as the framework: reuse
73+
Read https://devforth.io/admin/api/af-start/business/ first, then
74+
https://adminforth.dev/docs/tutorial/gettingStarted/ and
75+
https://adminforth.dev/llms.txt. Use AdminForth as the framework: reuse
7576
its screens, forms, filters and access rules, don't build a separate stack.
7677
Use pnpm and Node.js 24. For a new local example use SQLite, the documented
7778
create-app flow and Prisma migrations. Read the generated AGENTS.md, CLAUDE.md

‎documentation/src/pages/business/_brief.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const DAILY_PROMPTS = [
4848
'Which invoices are unpaid for more than 30 days?',
4949
];
5050

51-
export function buildPrompt(b: Brief): string {
51+
export function buildPrompt(b: Brief, startUrl: string): string {
5252
const source = SOURCES.find((s) => s.value === b.source).prompt;
5353
return `Help me build a small business application with AdminForth. Work with me in
5454
plain language. I don't have a developer: tell me exactly what to do myself,
@@ -68,8 +68,9 @@ through my agent; screens are for checking. Keep the first version small and
6868
ready for more parts later (orders, jobs, stock, invoices).
6969
7070
START HERE
71-
Read https://adminforth.dev/docs/tutorial/gettingStarted/ and
72-
https://adminforth.dev/llms.txt first. Use AdminForth as the framework: reuse
71+
Read ${startUrl} first, then
72+
https://adminforth.dev/docs/tutorial/gettingStarted/ and
73+
https://adminforth.dev/llms.txt. Use AdminForth as the framework: reuse
7374
its screens, forms, filters and access rules, don't build a separate stack.
7475
Use pnpm and Node.js 24. For a new local example use SQLite, the documented
7576
create-app flow and Prisma migrations. Read the generated AGENTS.md, CLAUDE.md

‎documentation/src/pages/business/index.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import {useState, type FormEvent, type ReactNode} from 'react';
1+
import {useEffect, useState, type FormEvent} from 'react';
22
import Head from '@docusaurus/Head';
33
import Link from '@docusaurus/Link';
44
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
55
import clsx from 'clsx';
66
import styles from './business.module.css';
77
import {buildPrompt, DAILY_PROMPTS, EXAMPLES, SOURCES, type Brief, type Source} from './_brief';
8+
import {sessionId, track} from '../../track';
89

910
// The business landing (/business/) for founders who run their business with AI agents. It has its own
1011
// header instead of the docs navbar, so it is not wrapped in the theme Layout. The screens are the drawn
@@ -15,6 +16,7 @@ const PAGE_DESCRIPTION = 'Agent-first business admin. Your coding agent builds t
1516
const FONTS_URL = 'https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@62..125,500..800&family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap';
1617
const BOOKING_URL = 'https://talk.devforth.io/book/30-minute-meeting';
1718
const GITHUB_URL = 'https://github.com/devforth/adminforth';
19+
const START_URL = 'https://devforth.io/admin/api/af-start/business/';
1820

1921
const PAINS = [
2022
{
@@ -411,7 +413,10 @@ function BriefBuilder(): JSX.Element {
411413
const [state, setState] = useState<ResultState>('idle');
412414
const [expanded, setExpanded] = useState(false);
413415
const [agent, setAgent] = useState<'claude' | 'codex'>('claude');
414-
const prompt = buildPrompt(brief);
416+
// the session id is in the browser only, the server render gets the link without it
417+
const [startUrl, setStartUrl] = useState(START_URL);
418+
useEffect(() => setStartUrl(`${START_URL}?s=${sessionId()}`), []);
419+
const prompt = buildPrompt(brief, startUrl);
415420

416421
const apply = (id: string) => {
417422
const {label: _label, ...fields} = EXAMPLES[id];
@@ -427,18 +432,21 @@ function BriefBuilder(): JSX.Element {
427432
};
428433
const prepare = (e: FormEvent) => {
429434
e.preventDefault();
435+
track('brief_prepared');
430436
setState('ready');
431437
document.getElementById('brief-ready-title')?.focus();
432438
};
433439
const copy = async () => {
434440
try {
435441
await navigator.clipboard.writeText(prompt);
442+
track('build_prompt_copied');
436443
setState('copied');
437444
} catch {
438445
setState('manual');
439446
}
440447
};
441448
const download = () => {
449+
track('brief_downloaded');
442450
const url = URL.createObjectURL(new Blob([prompt], {type: 'text/markdown'}));
443451
const a = Object.assign(document.createElement('a'), {href: url, download: 'my-business-brief.md'});
444452
a.click();

‎documentation/src/theme/Root.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import React, {type PropsWithChildren, useEffect} from 'react';
22
import {useLocation} from '@docusaurus/router';
33
import {measureOutboundClick, measurePageView} from '../openaiAds';
4+
import {trackOutboundClick, trackPageView} from '../track';
45

56
export default function Root({children}: PropsWithChildren): JSX.Element {
67
const location = useLocation();
78

89
useEffect(() => {
910
measurePageView();
11+
return trackPageView();
1012
}, [location.pathname, location.search]);
1113

1214
useEffect(() => {
1315
document.addEventListener('click', measureOutboundClick);
16+
document.addEventListener('click', trackOutboundClick);
1417

15-
return () => document.removeEventListener('click', measureOutboundClick);
18+
return () => {
19+
document.removeEventListener('click', measureOutboundClick);
20+
document.removeEventListener('click', trackOutboundClick);
21+
};
1622
}, []);
1723

1824
return <>{children}</>;

‎documentation/src/track.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Events of the site pages, sent to the Devforth back office (bo/api/af_site.ts) and shown on its Funnel page.
2+
// A session is a tab: its id, campaign and source are kept in sessionStorage from the first page it opened.
3+
4+
const EVENTS_URL = 'https://devforth.io/admin/api/af-events/';
5+
const ENGAGED_AFTER_MS = 30_000;
6+
7+
type Session = {id: string; campaign: string | null; source: string | null};
8+
9+
function session(): Session {
10+
const stored = sessionStorage.getItem('af_session');
11+
if (stored) {
12+
return JSON.parse(stored);
13+
}
14+
const params = new URLSearchParams(location.search);
15+
const created: Session = {
16+
id: crypto.randomUUID(),
17+
campaign: params.get('utm_campaign'),
18+
source: params.get('utm_source') ?? (document.referrer ? new URL(document.referrer).hostname : null),
19+
};
20+
sessionStorage.setItem('af_session', JSON.stringify(created));
21+
return created;
22+
}
23+
24+
export function track(event: string, props?: Record<string, string>): void {
25+
const {id, campaign, source} = session();
26+
const body = JSON.stringify({event, path: location.pathname, session_id: id, campaign, source, props});
27+
navigator.sendBeacon(EVENTS_URL, new Blob([body], {type: 'text/plain'}));
28+
}
29+
30+
export function sessionId(): string {
31+
return session().id;
32+
}
33+
34+
// page_view now, engaged after 30 seconds on the same page; the returned function cancels the timer on navigation
35+
export function trackPageView(): () => void {
36+
track('page_view');
37+
const timer = setTimeout(() => track('engaged'), ENGAGED_AFTER_MS);
38+
return () => clearTimeout(timer);
39+
}
40+
41+
export function trackOutboundClick(event: MouseEvent): void {
42+
const anchor = (event.target as Element).closest('a');
43+
if (anchor && anchor.host !== location.host && anchor.protocol.startsWith('http')) {
44+
track('outbound_click', {host: anchor.host});
45+
}
46+
}

0 commit comments

Comments
 (0)