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
1 change: 1 addition & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"check:queen-spec-mirrors": "node --experimental-strip-types qa/queen-spec-mirrors.mjs",
"check:queen-responsive": "node --experimental-strip-types qa/queen-responsive.mjs",
"check:spec-catalog": "node --experimental-strip-types qa/spec-catalog-contract.mjs",
"check:traffic-arrival": "node --experimental-strip-types qa/traffic-arrival-contract.mjs",
"core": "node scripts/spec-core.mjs",
"check:shared-core": "node --experimental-strip-types qa/shared-spec-core.mjs",
"prebuild": "npm run core -- index && node scripts/skills-core.mjs index && node scripts/agents-from-specs.mjs && node scripts/docs-from-specs.mjs && node scripts/viewport-from-spec.mjs && node scripts/onboarding-from-spec.mjs && node scripts/queen-wars-from-spec.mjs",
Expand Down
29 changes: 29 additions & 0 deletions apps/website/qa/traffic-arrival-contract.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// THE ARRIVAL HALF OF THE CHANNEL TAGS: a tagged visit to t27.ai is reported
// once, untagged visits never, and a failure never reaches the page.
// Run: node --experimental-strip-types qa/traffic-arrival-contract.mjs
import assert from 'node:assert/strict'
import { arrivalFromSearch, reportArrival, ARRIVAL_URL } from '../src/lib/trafficArrival.ts'

// The form the agents publish: the query BEFORE the hash route.
const blog = new URL('https://t27.ai/?utm_source=x&utm_medium=social&utm_campaign=blog&utm_content=gf-t#/blog/gf-t')
assert.deepEqual(arrivalFromSearch(blog.search), {
source: 'x', medium: 'social', campaign: 'blog', content: 'gf-t',
})
assert.equal(arrivalFromSearch(''), null, 'no tags, no arrival')
assert.equal(arrivalFromSearch('?utm_medium=social'), null, 'no source, no arrival')
// A query written after the hash is part of the route: not ours to read here.
assert.equal(arrivalFromSearch(new URL('https://t27.ai/#/blog/s?utm_source=x').search), null)

const store = new Map()
const storage = { getItem: k => store.get(k) ?? null, setItem: (k, v) => void store.set(k, v) }
const calls = []
const send = async (url, init) => { calls.push([url, JSON.parse(init.body)]); return new Response('{}') }
assert.equal(await reportArrival('?utm_source=reddit', send, storage), true)
assert.equal(await reportArrival('?utm_source=reddit', send, storage), false, 'once per session')
assert.equal(await reportArrival('', send, new Map() && { getItem: () => null, setItem: () => {} }), false)
assert.deepEqual(calls, [[ARRIVAL_URL, { source: 'reddit' }]])

const failing = async () => { throw new Error('offline') }
assert.equal(await reportArrival('?utm_source=threads', failing, { getItem: () => null, setItem: () => {} }), false)

console.log('traffic-arrival contract: 9 checks passed')
71 changes: 71 additions & 0 deletions apps/website/src/lib/trafficArrival.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* A VISITOR FROM A TAGGED LINK IS COUNTED ONCE, BY CHANNEL.
*
* The agents publish blog links with their channel in the address
* (`https://t27.ai/?utm_source=x&utm_campaign=blog&utm_content=<slug>#/blog/<slug>`,
* 999-multibots-telegraf render/src/agent/utm.ts). Tags on a link count
* nothing by themselves: this is the arrival half. On load the page reads
* the tags from its own query -- BEFORE the `#`, where HashRouter leaves
* them -- and tells the render service once per browser session, which
* keeps a count per (day, channel) and nothing else: no IP, no user id.
*
* No tags, no call. A failed call is dropped; the page never waits on it.
*/
export const ARRIVAL_URL =
'https://vibee-render-production.up.railway.app/api/traffic/arrival'

export interface ArrivalTags {
source: string
medium?: string
campaign?: string
content?: string
}

/** The utm tags of a query string, or null when it names no source. Pure. */
export function arrivalFromSearch(search: string): ArrivalTags | null {
const q = new URLSearchParams(search.startsWith('?') ? search : `?${search}`)
const source = (q.get('utm_source') ?? '').trim()
if (!source) return null
const tags: ArrivalTags = { source }
const more: Array<[string, 'medium' | 'campaign' | 'content']> = [
['utm_medium', 'medium'],
['utm_campaign', 'campaign'],
['utm_content', 'content'],
]
for (const [k, key] of more) {
const v = (q.get(k) ?? '').trim()
if (v) tags[key] = v
}
return tags
}

const ONCE_KEY = 'traffic:arrival-reported'

/** Report this visit's tags, once per browser session. Never throws. */
export async function reportArrival(
search: string = typeof window !== 'undefined' ? window.location.search : '',
send: typeof fetch = fetch,
storage: Pick<Storage, 'getItem' | 'setItem'> | null = typeof sessionStorage !== 'undefined'
? sessionStorage
: null
): Promise<boolean> {
const tags = arrivalFromSearch(search)
if (!tags) return false
try {
if (storage?.getItem(ONCE_KEY)) return false
storage?.setItem(ONCE_KEY, '1')
} catch {
/* no storage: count anyway */
}
try {
await send(ARRIVAL_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tags),
keepalive: true,
})
return true
} catch {
return false
}
}
6 changes: 6 additions & 0 deletions apps/website/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import GlobalStarfield from './components/GlobalStarfield.tsx'
import { handExplorerLinksToQueen } from './lib/queenFrame'
import { redirectLegacyQueen } from './lib/legacyQueenRedirect'
import { triIdentity } from './lib/triIdentity'
import { reportArrival } from './lib/trafficArrival'

// THE QUEEN MOVED to https://app.t27.ai/queen/, which now builds this same
// bundle and serves the board on the app's own origin. #/queen here is the old
Expand All @@ -27,6 +28,11 @@ import { triIdentity } from './lib/triIdentity'
// which calls the decision with both origins as input.
const leaving = redirectLegacyQueen()

// A visitor from a tagged link (utm_source=x, reddit, threads...) is counted
// once, by channel, where the agents' links land. Not for a page that is on
// its way to another address. See src/lib/trafficArrival.ts.
if (!leaving) void reportArrival()

// In a Queen tab's frame, a link to another Explorer switches the Queen's tab instead
// of navigating the frame under a rail that names a different one.
handExplorerLinksToQueen()
Expand Down
Loading