@iann29/rastro 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -9
- package/agent/integration.md +40 -19
- package/agent/manifest.json +13 -7
- package/agent/manifest.schema.json +18 -8
- package/dist/client/federation.d.ts +18 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +7 -1
- package/dist/client/federation.js.map +1 -1
- package/dist/client/identity.d.ts +11 -0
- package/dist/client/identity.d.ts.map +1 -0
- package/dist/client/identity.js +123 -0
- package/dist/client/identity.js.map +1 -0
- package/dist/client/index.d.ts +242 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +102 -3
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +87 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +30 -19
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/people.d.ts +79 -0
- package/dist/component/people.d.ts.map +1 -0
- package/dist/component/people.js +249 -0
- package/dist/component/people.js.map +1 -0
- package/dist/component/reports.d.ts +10 -4
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +9 -5
- package/dist/component/reports.js.map +1 -1
- package/dist/component/schema.d.ts +39 -0
- package/dist/component/schema.js +30 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators.d.ts +50 -0
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +27 -0
- package/dist/component/validators.js.map +1 -1
- package/dist/component/visitors.d.ts +38 -2
- package/dist/component/visitors.d.ts.map +1 -1
- package/dist/component/visitors.js +162 -42
- package/dist/component/visitors.js.map +1 -1
- package/dist/react/index.d.ts +9 -5
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +36 -5
- package/dist/react/index.js.map +1 -1
- package/dist/tracker/generated.d.ts +6 -6
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +6 -6
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts +2 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +41 -2
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/federation.md +24 -0
- package/docs/identity.md +307 -0
- package/docs/upgrading.md +58 -20
- package/llms.txt +6 -2
- package/package.json +5 -3
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +99 -0
- package/src/component/ingest.ts +46 -27
- package/src/component/people.ts +321 -0
- package/src/component/reports.ts +11 -4
- package/src/component/schema.ts +35 -0
- package/src/component/validators.ts +49 -0
- package/src/component/visitors.ts +232 -55
- package/src/tracker/generated.ts +6 -6
package/docs/upgrading.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Upgrading Amage Rastro
|
|
2
2
|
|
|
3
|
+
## People, attributes and identity continuity (0.6.0)
|
|
4
|
+
|
|
5
|
+
Install `@iann29/rastro@0.6.0` and deploy the host through its normal workflow.
|
|
6
|
+
|
|
7
|
+
- Additive `visitorProfiles` and `visitorAttributes` storage; no event/session
|
|
8
|
+
rewrite.
|
|
9
|
+
- Attributes are optional project-defined strings, numbers and booleans. Name,
|
|
10
|
+
email and attribute searches use indexes with bounded pagination.
|
|
11
|
+
- `identifyVisitor`, `identifyVisitorMutation` and authenticated
|
|
12
|
+
`identifyVisitorHttpAction` support any backend authentication. Existing
|
|
13
|
+
`linkVisitor` remains available.
|
|
14
|
+
- Re-export `getVisitorProfile` and `listPeople` from federation and refresh the
|
|
15
|
+
connection to advertise `visitorProfiles` and `people`. The dashboard
|
|
16
|
+
tolerates older hosts. Its Pessoas view lists profiles, filters attributes and
|
|
17
|
+
opens history.
|
|
18
|
+
- For profiles created before the directory, call the owner-authorized
|
|
19
|
+
`backfillVisitorProfiles({siteId})` until `isDone` is true (at most 64
|
|
20
|
+
profiles per call). This initializes search fields and attribute indexes;
|
|
21
|
+
timestamps and event history are preserved. Fresh installations need no
|
|
22
|
+
backfill.
|
|
23
|
+
- Funnels continue across linked pre-login and account IDs, including queued
|
|
24
|
+
alias events. Historical aggregates are not recomputed; steps must still
|
|
25
|
+
arrive in order.
|
|
26
|
+
- Map connections between routes in the same column bend around intermediate
|
|
27
|
+
nodes instead of drawing through them.
|
|
28
|
+
- Tracker `identify`/`reset` rotate sessions on login/logout without a reload;
|
|
29
|
+
`context` now includes `identified`. `RastroScript` synchronizes identity
|
|
30
|
+
changes and forwards native load/error events.
|
|
31
|
+
- The reference example's auth entrypoint is now
|
|
32
|
+
`analyticsIdentity:identifyVisitor`; configure its server-owned
|
|
33
|
+
`RASTRO_IDENTITY_SITE_ID`. Its optional HTTP route also requires
|
|
34
|
+
`RASTRO_IDENTITY_SERVER_KEY`.
|
|
35
|
+
- The tracker budget is now below 1,700 bytes gzip including identity switching.
|
|
36
|
+
|
|
37
|
+
See [the complete identity guide](identity.md) for integration and deployment
|
|
38
|
+
order.
|
|
39
|
+
|
|
3
40
|
Pin exact versions, read the changelog, and rehearse upgrades against a
|
|
4
41
|
non-production snapshot before changing a maintained deployment. `0.1.0` is the
|
|
5
42
|
first release without a prerelease tag; the alphas below it stay documented for
|
|
@@ -7,26 +44,27 @@ hosts still on them.
|
|
|
7
44
|
|
|
8
45
|
## Release matrix
|
|
9
46
|
|
|
10
|
-
| Current state | Required path | Success condition
|
|
11
|
-
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
12
|
-
| Fresh deployment | Install the intended release directly | Component deploys and `/rastro/health` succeeds
|
|
13
|
-
| `0.1.0-alpha.0` with maintained data | Install `0.1.0-alpha.1` first and wait for staged visitor-index backfills | Every staged index reports complete before installing `alpha.2`
|
|
14
|
-
| `0.1.0-alpha.1` | Wait for staged visitor-index backfills, then install `alpha.2` | Backfills complete before active readers deploy
|
|
15
|
-
| `0.1.0-alpha.2` with legacy `events` rows | Stay on `alpha.2` and run/resume migration | Durable success plus empty source
|
|
16
|
-
| `0.1.0-alpha.3` fresh/canonical deployment | Rehearse the next alpha normally | Schema, reports, tracker, and retention checks pass
|
|
17
|
-
| `0.1.0-alpha.3` through `alpha.5` with canonical data | Install `0.1.0-alpha.6` directly; Web Vitals tables are additive | Schema deploys and `/rastro/health` reports `webVitals: "optIn"`
|
|
18
|
-
| `0.1.0-alpha.6` | Install `0.1.0-alpha.7` directly; no schema change | `GET /rastro/tracker.js` contains `sentAt`
|
|
19
|
-
| `0.1.0-alpha.7` | Install `0.1.0-alpha.8` directly; site map tables are additive; re-export `siteMap` | `/rastro/health` reports `siteMap: "dailyRollup"`
|
|
20
|
-
| `0.1.0-alpha.8` | Install `0.1.0-alpha.10` directly; no schema change; the tracker changes | `/rastro/health` reports `liveDepartures: "leaveBeacon"`
|
|
21
|
-
| `0.1.0-alpha.9` | Install `0.1.0-alpha.10` directly; `alpha.9` is deprecated — it rejected its own leave beacons | The final page-close `POST /rastro/events` returns 200
|
|
22
|
-
| `0.1.0-alpha.10` | Install `0.1.0-alpha.12` directly; new tables and fields are additive; re-export the configure functions to opt in | `/rastro/health` reports `tracker.version: "0.1.0-alpha.12"`
|
|
23
|
-
| `0.1.0-alpha.11` | Install `0.1.0-alpha.12` directly; same code, the package now ships `src/tracker/generated.ts` | A `convex/` test importing `@iann29/rastro/test` typechecks on push
|
|
24
|
-
| `0.1.0-alpha.12` | Install `0.1.0` directly; every schema change is an optional field or a new table; add `analytics:public` to a grant only to allow public links | `/rastro/health` reports `tracker.version: "0.1.0"`
|
|
25
|
-
| `0.1.0` | Install `0.2.0` directly; add the optional host callback and grant version to enable transfers | Health reports `0.2.0`; opted-in connectors advertise `transfer`
|
|
26
|
-
| `0.2.0` | Install `0.3.0` directly; no new table; export `getSession` and `revenueSummary` from the federation module | Health reports `0.3.0`; the manifest lists `sessionDetails`
|
|
27
|
-
| `0.3.0` | Install `0.4.0` directly; no re-export; the deploy rebuilds the `reportRollupQueue` index and the tracker changes | Health reports `0.4.0`; a hidden tab leaves **Ao vivo** in ~10 s
|
|
28
|
-
| `0.4.0` | Install `0.5.0` directly; every schema change is an optional field or a new table; export `listTrackedLinks`, `createTrackedLink`, `updateTrackedLink` and `deleteTrackedLink` from the federation module; reverify — the manifest advertises `trackedLinks` | Health reports `0.5.0`; the manifest lists `trackedLinks`
|
|
29
|
-
|
|
|
47
|
+
| Current state | Required path | Success condition |
|
|
48
|
+
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
49
|
+
| Fresh deployment | Install the intended release directly | Component deploys and `/rastro/health` succeeds |
|
|
50
|
+
| `0.1.0-alpha.0` with maintained data | Install `0.1.0-alpha.1` first and wait for staged visitor-index backfills | Every staged index reports complete before installing `alpha.2` |
|
|
51
|
+
| `0.1.0-alpha.1` | Wait for staged visitor-index backfills, then install `alpha.2` | Backfills complete before active readers deploy |
|
|
52
|
+
| `0.1.0-alpha.2` with legacy `events` rows | Stay on `alpha.2` and run/resume migration | Durable success plus empty source |
|
|
53
|
+
| `0.1.0-alpha.3` fresh/canonical deployment | Rehearse the next alpha normally | Schema, reports, tracker, and retention checks pass |
|
|
54
|
+
| `0.1.0-alpha.3` through `alpha.5` with canonical data | Install `0.1.0-alpha.6` directly; Web Vitals tables are additive | Schema deploys and `/rastro/health` reports `webVitals: "optIn"` |
|
|
55
|
+
| `0.1.0-alpha.6` | Install `0.1.0-alpha.7` directly; no schema change | `GET /rastro/tracker.js` contains `sentAt` |
|
|
56
|
+
| `0.1.0-alpha.7` | Install `0.1.0-alpha.8` directly; site map tables are additive; re-export `siteMap` | `/rastro/health` reports `siteMap: "dailyRollup"` |
|
|
57
|
+
| `0.1.0-alpha.8` | Install `0.1.0-alpha.10` directly; no schema change; the tracker changes | `/rastro/health` reports `liveDepartures: "leaveBeacon"` |
|
|
58
|
+
| `0.1.0-alpha.9` | Install `0.1.0-alpha.10` directly; `alpha.9` is deprecated — it rejected its own leave beacons | The final page-close `POST /rastro/events` returns 200 |
|
|
59
|
+
| `0.1.0-alpha.10` | Install `0.1.0-alpha.12` directly; new tables and fields are additive; re-export the configure functions to opt in | `/rastro/health` reports `tracker.version: "0.1.0-alpha.12"` |
|
|
60
|
+
| `0.1.0-alpha.11` | Install `0.1.0-alpha.12` directly; same code, the package now ships `src/tracker/generated.ts` | A `convex/` test importing `@iann29/rastro/test` typechecks on push |
|
|
61
|
+
| `0.1.0-alpha.12` | Install `0.1.0` directly; every schema change is an optional field or a new table; add `analytics:public` to a grant only to allow public links | `/rastro/health` reports `tracker.version: "0.1.0"` |
|
|
62
|
+
| `0.1.0` | Install `0.2.0` directly; add the optional host callback and grant version to enable transfers | Health reports `0.2.0`; opted-in connectors advertise `transfer` |
|
|
63
|
+
| `0.2.0` | Install `0.3.0` directly; no new table; export `getSession` and `revenueSummary` from the federation module | Health reports `0.3.0`; the manifest lists `sessionDetails` |
|
|
64
|
+
| `0.3.0` | Install `0.4.0` directly; no re-export; the deploy rebuilds the `reportRollupQueue` index and the tracker changes | Health reports `0.4.0`; a hidden tab leaves **Ao vivo** in ~10 s |
|
|
65
|
+
| `0.4.0` | Install `0.5.0` directly; every schema change is an optional field or a new table; export `listTrackedLinks`, `createTrackedLink`, `updateTrackedLink` and `deleteTrackedLink` from the federation module; reverify — the manifest advertises `trackedLinks` | Health reports `0.5.0`; the manifest lists `trackedLinks` |
|
|
66
|
+
| `0.5.0` | Install `0.6.0`; export `listPeople` and `getVisitorProfile`, deploy, and reverify. Use the host identity resolver to populate profiles. | Health reports `0.6.0`; the manifest lists `people` and `visitorProfiles` |
|
|
67
|
+
| Any release whose packed declarations lack federation | Stop and request an eligible exact registry release | Packed declarations contain the federation exports |
|
|
30
68
|
|
|
31
69
|
Do not upgrade a populated legacy deployment directly to a release that removes
|
|
32
70
|
the `events` table. Schema acceptance alone does not prove that telemetry was
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Amage Rastro
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Real-time product and web analytics: identified people, journeys, conversions and retention on Convex. Telemetry stays in each product's Convex or Synapse deployment; the central dashboard reads only an explicitly granted report surface, and configures a site only when its host's grant opts into the configure scope.
|
|
4
4
|
|
|
5
5
|
Canonical documentation: https://www.amagerastro.com/docs/
|
|
6
6
|
Machine manifest: https://www.amagerastro.com/agent/manifest.json
|
|
@@ -16,7 +16,7 @@ Do not infer federation availability from this website, a dist-tag, or a source
|
|
|
16
16
|
## Architecture
|
|
17
17
|
|
|
18
18
|
- Tracker and telemetry: browser -> customer HTTP-actions URL -> customer Rastro component.
|
|
19
|
-
- The tracker is 2,
|
|
19
|
+
- The tracker is 2,999 bytes raw and 1,565 gzip. It sends the click-ID key **names** of the landing URL in `clid` (`gclid`, `fbclid`, `ttclid`…), at most eight, never a value, and the referrer as scheme and host, so an `android-app://<package>` referrer keeps its package.
|
|
20
20
|
- Tracked links (0.5.0+): browser -> customer HTTP-actions URL `/rastro/l/<slug>` -> `302` (`Cache-Control: no-store`) to a destination on one of the site's domains, with `utm_source` (platform), `utm_medium` (channel), `utm_campaign` (campaign, or the slug) and `utm_content` (slug) appended; the destination's own query and `utm_*` keys stay. Unknown or disabled slug: `404`. Clicks count in four-shard daily counters; self-declared bots are redirected and counted apart.
|
|
21
21
|
- Optional field Web Vitals (alpha.6+): a second browser module at `/rastro/vitals.js` reports LCP, CLS, INP, FCP, and TTFB to the same events endpoint, into bounded daily histograms; it needs the base tracker on the page.
|
|
22
22
|
- Reactive dashboard reads: amagerastro.com browser -> customer functions URL -> `rastroFederation:*`.
|
|
@@ -76,3 +76,7 @@ Do not infer federation availability from this website, a dist-tag, or a source
|
|
|
76
76
|
- `dataCoverage` echoes the authorized site ID but returns no event, visitor, or session IDs.
|
|
77
77
|
- `vitalsReport` returns p75, averages, and exact good/needs-improvement/poor counts per metric, page, and device — never individual measurements.
|
|
78
78
|
- `siteMap` returns per-route totals (pageviews, estimated visitors, entries, derived exits and bounces, time on page) and route-to-route transition counts, with numeric, UUID, and long hexadecimal segments collapsed to `:id` — never sessions, visitor IDs, or query strings. `liveVisitors` adds `previousPath` and `movedAt` when a visitor moves between routes, and since 0.1.0 the intent facts of the session — `entryPath`, `returning`, `utmCampaign`, `affiliateSlug`, `conversionCount`, and the furthest `funnel` step — which `visitorHeat` from the package turns into the dashboard's hot/warm/cold score.
|
|
79
|
+
|
|
80
|
+
## People and identity (0.6.0+)
|
|
81
|
+
|
|
82
|
+
Use the [identity guide](https://www.amagerastro.com/docs/reference/identity.md) for Better Auth, custom Convex auth and authenticated HTTP from other backends. `Rastro.identifyVisitorMutation` resolves the current user on the host; `identifyVisitorHttpAction` authorizes external servers. `getVisitorProfile` requires reader access and is advertised as `visitorProfiles`. `listPeople` adds the optional `people` directory with indexed name/email/ID prefix search and exact filters on project-defined string, number or boolean attributes. Existing profiles need the bounded host-authorized search backfill described in the guide. The browser applies the returned ID with `identify` and calls `reset` on logout.
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iann29/rastro",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Real-time product and web analytics for Convex: identified people, journeys, revenue, goals, funnels, affiliates, and a lightweight browser tracker.",
|
|
4
4
|
"repository": "github:amageweb/amage-rastro",
|
|
5
5
|
"homepage": "https://github.com/amageweb/amage-rastro#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/amageweb/amage-rastro/issues"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.6.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"convex-component",
|
|
17
17
|
"analytics",
|
|
18
18
|
"realtime",
|
|
19
|
-
"
|
|
19
|
+
"product-analytics",
|
|
20
20
|
"cookieless",
|
|
21
21
|
"session-journey",
|
|
22
22
|
"conversion-tracking",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"typecheck": "tsc --noEmit && tsc -p control-plane && tsc -p control-plane/convex && tsc -p example/convex",
|
|
47
47
|
"lint": "eslint .",
|
|
48
48
|
"test": "vitest run --typecheck",
|
|
49
|
+
"test:layout": "playwright test -c control-plane/playwright.layout.config.ts",
|
|
49
50
|
"test:watch": "vitest --typecheck --clearScreen false",
|
|
50
51
|
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
|
51
52
|
"test:coverage": "vitest run --coverage --coverage.reporter=text",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"docs/federation.md",
|
|
69
70
|
"docs/federation-setup.md",
|
|
70
71
|
"docs/upgrading.md",
|
|
72
|
+
"docs/identity.md",
|
|
71
73
|
"docs/benchmarks/*.md",
|
|
72
74
|
"assets/rastro-live-atlas.png"
|
|
73
75
|
],
|
|
@@ -25,6 +25,7 @@ import type * as ingest from "../ingest.js";
|
|
|
25
25
|
import type * as live from "../live.js";
|
|
26
26
|
import type * as localTime from "../localTime.js";
|
|
27
27
|
import type * as origin from "../origin.js";
|
|
28
|
+
import type * as people from "../people.js";
|
|
28
29
|
import type * as platforms from "../platforms.js";
|
|
29
30
|
import type * as reports from "../reports.js";
|
|
30
31
|
import type * as retention from "../retention.js";
|
|
@@ -63,6 +64,7 @@ const fullApi: ApiFromModules<{
|
|
|
63
64
|
live: typeof live;
|
|
64
65
|
localTime: typeof localTime;
|
|
65
66
|
origin: typeof origin;
|
|
67
|
+
people: typeof people;
|
|
66
68
|
platforms: typeof platforms;
|
|
67
69
|
reports: typeof reports;
|
|
68
70
|
retention: typeof retention;
|
|
@@ -241,6 +241,46 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
241
241
|
Name
|
|
242
242
|
>;
|
|
243
243
|
};
|
|
244
|
+
people: {
|
|
245
|
+
backfillSearch: FunctionReference<
|
|
246
|
+
"mutation",
|
|
247
|
+
"internal",
|
|
248
|
+
{ siteId: string },
|
|
249
|
+
{ isDone: boolean; updated: number },
|
|
250
|
+
Name
|
|
251
|
+
>;
|
|
252
|
+
list: FunctionReference<
|
|
253
|
+
"query",
|
|
254
|
+
"internal",
|
|
255
|
+
{
|
|
256
|
+
filter?:
|
|
257
|
+
| { field: "name" | "email" | "visitorId"; prefix: string }
|
|
258
|
+
| {
|
|
259
|
+
field: "attribute";
|
|
260
|
+
key: string;
|
|
261
|
+
value: string | number | boolean;
|
|
262
|
+
};
|
|
263
|
+
paginationOpts: { cursor: string | null; numItems: number };
|
|
264
|
+
siteId: string;
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
continueCursor: string;
|
|
268
|
+
isDone: boolean;
|
|
269
|
+
page: Array<{
|
|
270
|
+
_creationTime: number;
|
|
271
|
+
_id: string;
|
|
272
|
+
attributes?: Record<string, string | number | boolean>;
|
|
273
|
+
email?: string;
|
|
274
|
+
identifiedAt: number;
|
|
275
|
+
name?: string;
|
|
276
|
+
siteId: string;
|
|
277
|
+
updatedAt: number;
|
|
278
|
+
visitorId: string;
|
|
279
|
+
}>;
|
|
280
|
+
},
|
|
281
|
+
Name
|
|
282
|
+
>;
|
|
283
|
+
};
|
|
244
284
|
reports: {
|
|
245
285
|
affiliatesReport: FunctionReference<
|
|
246
286
|
"query",
|
|
@@ -415,6 +455,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
415
455
|
os: string;
|
|
416
456
|
pageviewCount: number;
|
|
417
457
|
platform?: string;
|
|
458
|
+
profile?: {
|
|
459
|
+
attributes?: Record<string, string | number | boolean>;
|
|
460
|
+
email?: string;
|
|
461
|
+
identifiedAt: number;
|
|
462
|
+
name?: string;
|
|
463
|
+
updatedAt: number;
|
|
464
|
+
visitorId: string;
|
|
465
|
+
};
|
|
418
466
|
referrer?: string;
|
|
419
467
|
revenueCents: number;
|
|
420
468
|
sessionId: string;
|
|
@@ -575,6 +623,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
575
623
|
os: string;
|
|
576
624
|
pageviewCount: number;
|
|
577
625
|
platform?: string;
|
|
626
|
+
profile?: {
|
|
627
|
+
attributes?: Record<string, string | number | boolean>;
|
|
628
|
+
email?: string;
|
|
629
|
+
identifiedAt: number;
|
|
630
|
+
name?: string;
|
|
631
|
+
updatedAt: number;
|
|
632
|
+
visitorId: string;
|
|
633
|
+
};
|
|
578
634
|
referrer?: string;
|
|
579
635
|
revenueCents: number;
|
|
580
636
|
sessionId: string;
|
|
@@ -617,6 +673,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
617
673
|
pageviewCount: number;
|
|
618
674
|
path: string;
|
|
619
675
|
previousPath?: string;
|
|
676
|
+
profile?: {
|
|
677
|
+
attributes?: Record<string, string | number | boolean>;
|
|
678
|
+
email?: string;
|
|
679
|
+
identifiedAt: number;
|
|
680
|
+
name?: string;
|
|
681
|
+
updatedAt: number;
|
|
682
|
+
visitorId: string;
|
|
683
|
+
};
|
|
620
684
|
returning?: boolean;
|
|
621
685
|
sessionId: string;
|
|
622
686
|
siteId: string;
|
|
@@ -1292,6 +1356,41 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1292
1356
|
>;
|
|
1293
1357
|
};
|
|
1294
1358
|
visitors: {
|
|
1359
|
+
getProfile: FunctionReference<
|
|
1360
|
+
"query",
|
|
1361
|
+
"internal",
|
|
1362
|
+
{ siteId: string; visitorId: string },
|
|
1363
|
+
{
|
|
1364
|
+
attributes?: Record<string, string | number | boolean>;
|
|
1365
|
+
email?: string;
|
|
1366
|
+
identifiedAt: number;
|
|
1367
|
+
name?: string;
|
|
1368
|
+
updatedAt: number;
|
|
1369
|
+
visitorId: string;
|
|
1370
|
+
} | null,
|
|
1371
|
+
Name
|
|
1372
|
+
>;
|
|
1373
|
+
identify: FunctionReference<
|
|
1374
|
+
"mutation",
|
|
1375
|
+
"internal",
|
|
1376
|
+
{
|
|
1377
|
+
attributes?: Record<string, string | number | boolean | null>;
|
|
1378
|
+
email?: string | null;
|
|
1379
|
+
name?: string | null;
|
|
1380
|
+
previousVisitorId?: string;
|
|
1381
|
+
siteId: string;
|
|
1382
|
+
visitorId: string;
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
attributes?: Record<string, string | number | boolean>;
|
|
1386
|
+
email?: string;
|
|
1387
|
+
identifiedAt: number;
|
|
1388
|
+
name?: string;
|
|
1389
|
+
updatedAt: number;
|
|
1390
|
+
visitorId: string;
|
|
1391
|
+
},
|
|
1392
|
+
Name
|
|
1393
|
+
>;
|
|
1295
1394
|
link: FunctionReference<
|
|
1296
1395
|
"mutation",
|
|
1297
1396
|
"internal",
|
package/src/component/ingest.ts
CHANGED
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
} from "./rollupStore.js";
|
|
90
90
|
import { localDayStart, localDayTimezone } from "./localTime.js";
|
|
91
91
|
import { classifyOrigin, platformDimensionValue } from "./origin.js";
|
|
92
|
+
import { resolveVisitorIdentities } from "./visitors.js";
|
|
92
93
|
|
|
93
94
|
type DimensionType =
|
|
94
95
|
| "source"
|
|
@@ -217,6 +218,7 @@ type BatchState = {
|
|
|
217
218
|
CachedRow<CachedDocument<"visitorAttributions">>
|
|
218
219
|
>;
|
|
219
220
|
funnelProgress: Map<string, CachedRow<CachedDocument<"funnelProgress">>>;
|
|
221
|
+
funnelIdentities: Map<string, string[]>;
|
|
220
222
|
eventBatches: EventBatchBuffer;
|
|
221
223
|
liveSweepSites: Set<string>;
|
|
222
224
|
};
|
|
@@ -286,6 +288,7 @@ export const ingestBatch = mutation({
|
|
|
286
288
|
liveSessions: new Map(),
|
|
287
289
|
visitorAttributions: new Map(),
|
|
288
290
|
funnelProgress: new Map(),
|
|
291
|
+
funnelIdentities: new Map(),
|
|
289
292
|
eventBatches: new Map(),
|
|
290
293
|
liveSweepSites: new Set(),
|
|
291
294
|
};
|
|
@@ -1927,46 +1930,62 @@ async function advanceFunnel(
|
|
|
1927
1930
|
reportRollupDeltas: ReportRollupDeltas,
|
|
1928
1931
|
batchState?: BatchState,
|
|
1929
1932
|
): Promise<FunnelReach | undefined> {
|
|
1930
|
-
|
|
1933
|
+
let identities = batchState?.funnelIdentities.get(event.visitorId);
|
|
1934
|
+
if (!identities) {
|
|
1935
|
+
identities = await resolveVisitorIdentities(ctx, siteId, event.visitorId);
|
|
1936
|
+
for (const id of identities)
|
|
1937
|
+
batchState?.funnelIdentities.set(id, identities);
|
|
1938
|
+
}
|
|
1939
|
+
const cacheKey = JSON.stringify([funnel._id, identities[0]]);
|
|
1931
1940
|
let progressState = batchState?.funnelProgress.get(cacheKey);
|
|
1932
|
-
if (
|
|
1933
|
-
const
|
|
1934
|
-
.
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
.
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1941
|
+
if (!progressState) {
|
|
1942
|
+
const candidates = await Promise.all(
|
|
1943
|
+
identities.map((visitorId) =>
|
|
1944
|
+
ctx.db
|
|
1945
|
+
.query("funnelProgress")
|
|
1946
|
+
.withIndex("by_siteId_and_funnelId_and_visitorId", (range) =>
|
|
1947
|
+
range
|
|
1948
|
+
.eq("siteId", siteId)
|
|
1949
|
+
.eq("funnelId", funnel._id)
|
|
1950
|
+
.eq("visitorId", visitorId),
|
|
1951
|
+
)
|
|
1952
|
+
.unique(),
|
|
1953
|
+
),
|
|
1954
|
+
);
|
|
1955
|
+
// Reuse the furthest attempt still inside its window. Already-counted
|
|
1956
|
+
// attempts are not replayed or merged into historical aggregates.
|
|
1957
|
+
const current =
|
|
1958
|
+
candidates
|
|
1959
|
+
.filter((row) => row !== null)
|
|
1960
|
+
.sort((a, b) => {
|
|
1961
|
+
const activeA =
|
|
1962
|
+
event.timestamp - a.startedAt <= funnel.conversionWindowMs;
|
|
1963
|
+
const activeB =
|
|
1964
|
+
event.timestamp - b.startedAt <= funnel.conversionWindowMs;
|
|
1965
|
+
return (
|
|
1966
|
+
Number(activeB) - Number(activeA) ||
|
|
1967
|
+
(activeA ? b.currentStep - a.currentStep : 0) ||
|
|
1968
|
+
b.lastStepAt - a.lastStepAt
|
|
1969
|
+
);
|
|
1970
|
+
})[0] ?? null;
|
|
1942
1971
|
progressState = { current, dirty: false };
|
|
1943
|
-
batchState
|
|
1972
|
+
batchState?.funnelProgress.set(cacheKey, progressState);
|
|
1944
1973
|
}
|
|
1945
|
-
const progress = progressState
|
|
1946
|
-
? progressState.current
|
|
1947
|
-
: await ctx.db
|
|
1948
|
-
.query("funnelProgress")
|
|
1949
|
-
.withIndex("by_siteId_and_funnelId_and_visitorId", (range) =>
|
|
1950
|
-
range
|
|
1951
|
-
.eq("siteId", siteId)
|
|
1952
|
-
.eq("funnelId", funnel._id)
|
|
1953
|
-
.eq("visitorId", event.visitorId),
|
|
1954
|
-
)
|
|
1955
|
-
.unique();
|
|
1974
|
+
const progress = progressState.current;
|
|
1956
1975
|
const matchesFirst = stepMatches(funnel.steps[0], event);
|
|
1957
1976
|
if (!progress) {
|
|
1958
1977
|
if (!matchesFirst) return;
|
|
1959
1978
|
const fields: Omit<Doc<"funnelProgress">, "_id" | "_creationTime"> = {
|
|
1960
1979
|
siteId,
|
|
1961
1980
|
funnelId: funnel._id,
|
|
1962
|
-
visitorId:
|
|
1981
|
+
visitorId: identities[0],
|
|
1963
1982
|
sessionId: event.sessionId,
|
|
1964
1983
|
currentStep: 1,
|
|
1965
1984
|
startedAt: event.timestamp,
|
|
1966
1985
|
lastStepAt: event.timestamp,
|
|
1967
1986
|
};
|
|
1968
1987
|
const progressId = await ctx.db.insert("funnelProgress", fields);
|
|
1969
|
-
if (
|
|
1988
|
+
if (batchState) {
|
|
1970
1989
|
progressState.current = {
|
|
1971
1990
|
...fields,
|
|
1972
1991
|
_id: progressId,
|
|
@@ -1997,7 +2016,7 @@ async function advanceFunnel(
|
|
|
1997
2016
|
lastStepAt: event.timestamp,
|
|
1998
2017
|
};
|
|
1999
2018
|
delete updated.completedAt;
|
|
2000
|
-
if (
|
|
2019
|
+
if (batchState) {
|
|
2001
2020
|
progressState.current = updated;
|
|
2002
2021
|
progressState.dirty = true;
|
|
2003
2022
|
} else {
|
|
@@ -2031,7 +2050,7 @@ async function advanceFunnel(
|
|
|
2031
2050
|
lastStepAt: event.timestamp,
|
|
2032
2051
|
...(completed ? { completedAt: event.timestamp } : {}),
|
|
2033
2052
|
};
|
|
2034
|
-
if (
|
|
2053
|
+
if (batchState) {
|
|
2035
2054
|
progressState.current = { ...progress, ...updates };
|
|
2036
2055
|
progressState.dirty = true;
|
|
2037
2056
|
} else {
|