@agent-native/core-corpus 0.201.0-nightly-20261005153421 → 0.201.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +14 -4
- package/corpus/templates/analytics/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/analytics/CHANGELOG.md +6 -0
- package/corpus/templates/analytics/README.md +2 -2
- package/corpus/templates/analytics/changelog/2026-10-02-events-with-a-very-long-name-app-or-page-path-no-longer-caus.md +6 -0
- package/corpus/templates/analytics/package.json +1 -0
- package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +9 -19
- package/corpus/templates/analytics/server/handlers/session-replay.ts +34 -47
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +57 -36
- package/corpus/templates/analytics/server/lib/indexed-text.ts +53 -0
- package/corpus/templates/analytics/server/lib/request-errors.ts +73 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +6 -16
- package/corpus/templates/analytics/server/lib/session-replay.ts +12 -2
- package/corpus/templates/analytics/server/plugins/auth.ts +2 -1
- package/corpus/templates/analytics/server/plugins/otel.ts +6 -0
- package/corpus/templates/assets/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/assets/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/assets/CHANGELOG.md +7 -0
- package/corpus/templates/assets/README.md +2 -2
- package/corpus/templates/assets/package.json +1 -0
- package/corpus/templates/assets/server/plugins/auth.ts +2 -1
- package/corpus/templates/assets/server/plugins/otel.ts +6 -0
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +2 -1
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +5 -4
- package/corpus/templates/brain/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/brain/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/brain/CHANGELOG.md +6 -0
- package/corpus/templates/brain/README.md +12 -8
- package/corpus/templates/brain/changelog/2026-10-05-private-slack-channels-are-searchable-org-wide.md +6 -0
- package/corpus/templates/brain/package.json +1 -0
- package/corpus/templates/brain/server/lib/audiences.ts +1 -98
- package/corpus/templates/brain/server/lib/connectors.ts +2 -166
- package/corpus/templates/brain/server/plugins/otel.ts +6 -0
- package/corpus/templates/calendar/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/calendar/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/calendar/CHANGELOG.md +7 -0
- package/corpus/templates/calendar/README.md +2 -2
- package/corpus/templates/calendar/package.json +1 -0
- package/corpus/templates/calendar/server/plugins/auth.ts +2 -1
- package/corpus/templates/calendar/server/plugins/otel.ts +6 -0
- package/corpus/templates/chat/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/chat/README.md +2 -2
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +9 -0
- package/corpus/templates/clips/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/clips/CHANGELOG.md +24 -0
- package/corpus/templates/clips/README.md +2 -2
- package/corpus/templates/clips/actions/lib/audio-chunk-list.ts +23 -0
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +187 -292
- package/corpus/templates/clips/actions/lib/chunked-transcription.ts +151 -0
- package/corpus/templates/clips/actions/lib/recording-audio-source.ts +191 -0
- package/corpus/templates/clips/actions/request-transcript.ts +81 -135
- package/corpus/templates/clips/actions/search-recordings.ts +19 -5
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +5 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -1
- package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -0
- package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
- package/corpus/templates/clips/app/lib/local-recording-upload.ts +13 -6
- package/corpus/templates/clips/app/lib/recording-section.ts +1 -1
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +7 -5
- package/corpus/templates/clips/app/routes/record.tsx +16 -6
- package/corpus/templates/clips/changelog/2026-10-02-long-uploads-an-hour-or-more-now-transcribe-reliably-instead.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-confirming-discard-recording-now-returns-you-to-your-library.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recording-now-removes-its-failed-upload-card-fr.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recovered-recording-no-longer-warns-that-it-s-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-search-results-identify-recordings-in-trash-and-open-them-wi.md +6 -0
- package/corpus/templates/clips/package.json +1 -0
- package/corpus/templates/clips/server/lib/builder-cdn.ts +5 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +2 -2
- package/corpus/templates/clips/server/plugins/auth.ts +2 -1
- package/corpus/templates/clips/server/plugins/otel.ts +6 -0
- package/corpus/templates/clips/shared/transcript-failure.ts +5 -0
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
- package/corpus/templates/content/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/content/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/content/CHANGELOG.md +41 -0
- package/corpus/templates/content/README.md +2 -2
- package/corpus/templates/content/actions/_database-navigation.ts +523 -344
- package/corpus/templates/content/actions/_database-row-mutation.ts +44 -10
- package/corpus/templates/content/actions/_database-setup-discovery.ts +9 -3
- package/corpus/templates/content/actions/_database-utils.ts +299 -271
- package/corpus/templates/content/actions/_property-utils.ts +293 -190
- package/corpus/templates/content/actions/_relation-values.ts +3 -1
- package/corpus/templates/content/actions/get-content-database.ts +1 -0
- package/corpus/templates/content/actions/query-content-database-items.ts +32 -5
- package/corpus/templates/content/actions/set-document-property.ts +17 -11
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +21 -6
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +127 -43
- package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +159 -50
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +12 -28
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +6 -1
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +139 -70
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +16 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +180 -75
- package/corpus/templates/content/app/components/editor/database/DatabaseViewSkeleton.tsx +76 -0
- package/corpus/templates/content/app/components/editor/database/navigation-state.ts +60 -2
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +41 -42
- package/corpus/templates/content/app/components/editor/document-editor-layout.ts +18 -0
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +11 -3
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +15 -7
- package/corpus/templates/content/app/components/editor/suggestions/model.ts +13 -0
- package/corpus/templates/content/app/components/editor/suggestions/read-only-blocks.ts +136 -0
- package/corpus/templates/content/app/components/editor/table-view.ts +73 -0
- package/corpus/templates/content/app/components/layout/ContentStartupShell.tsx +1 -1
- package/corpus/templates/content/app/components/layout/Layout.tsx +5 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +57 -16
- package/corpus/templates/content/app/components/sidebar/MovePageDialog.tsx +4 -4
- package/corpus/templates/content/app/components/sidebar/SidebarNavigationRow.tsx +34 -10
- package/corpus/templates/content/app/global.css +16 -8
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +14 -13
- package/corpus/templates/content/app/hooks/use-content-database.ts +72 -9
- package/corpus/templates/content/app/hooks/use-content-personal-navigation.ts +7 -3
- package/corpus/templates/content/app/hooks/use-documents.ts +27 -0
- package/corpus/templates/content/app/i18n-data.ts +3 -2
- package/corpus/templates/content/app/lib/content-landing.ts +3 -31
- package/corpus/templates/content/app/lib/files-navigation.ts +182 -10
- package/corpus/templates/content/app/lib/files-root-hint.ts +20 -32
- package/corpus/templates/content/app/lib/page-startup-hints.ts +108 -0
- package/corpus/templates/content/app/lib/private-link-redirect.ts +25 -0
- package/corpus/templates/content/app/lib/sidebar-layout-hint.ts +34 -1
- package/corpus/templates/content/app/lib/startup-timing.ts +2 -0
- package/corpus/templates/content/app/root.tsx +3 -1
- package/corpus/templates/content/app/routes/_app.home.tsx +10 -19
- package/corpus/templates/content/app/routes/p.$id.tsx +23 -5
- package/corpus/templates/content/changelog/2026-10-02-a-page-link-you-cant-open-now-says-so-instead-of-opening-an.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-after-a-refresh-the-files-sidebar-shows-the-open-page-s-fold.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-large-collections-open-faster.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-pages-stay-inside-the-window-at-every-width-long-inline-code.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-pages-with-open-comments-and-collections-with-sorted-or-filt.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-suggest-edits-works-on-pages-written-by-agents-including-pag.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-while-you-suggest-edits-tables-and-other-blocks-that-suggest.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-collection-view-context-now-survives-quick-page-changes-when.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-collection-views-keep-their-saved-rows-accurate-during-refre.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-exact-saved-view-links-keep-collection-retry-available.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-expanding-a-files-folder-now-shares-one-navigation-read-acro.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-files-navigation-retries-temporary-failures-consistently.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-shared-page-owners-keep-edit-and-manage-controls-in-files-an.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-tagging-an-ai-in-a-comment-works-again-instead-of-failing-ri.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-the-home-loading-shell-keeps-the-layout-of-your-last-opened-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-typing-and-a-space-or-inserting-a-toggle-or-callout-no-longe.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +4 -1
- package/corpus/templates/content/e2e/playwright.config.ts +1 -1
- package/corpus/templates/content/package.json +2 -1
- package/corpus/templates/content/scripts/seed-perf-database.mjs +600 -0
- package/corpus/templates/content/scripts/trace-startup.mjs +25 -3
- package/corpus/templates/content/server/db/index.ts +1 -1
- package/corpus/templates/content/server/lib/suggested-edits.ts +23 -26
- package/corpus/templates/content/server/plugins/auth.ts +2 -1
- package/corpus/templates/content/server/plugins/otel.ts +6 -0
- package/corpus/templates/content/shared/api.ts +17 -0
- package/corpus/templates/content/shared/suggestion-diff.ts +124 -38
- package/corpus/templates/content/shared/suggestion-formatting.ts +134 -3
- package/corpus/templates/crm/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/crm/package.json +1 -0
- package/corpus/templates/crm/server/plugins/otel.ts +6 -0
- package/corpus/templates/design/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/design/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/design/CHANGELOG.md +41 -0
- package/corpus/templates/design/README.md +2 -2
- package/corpus/templates/design/actions/list-designs.ts +11 -4
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +110 -32
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +28 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -0
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +79 -0
- package/corpus/templates/design/app/components/layout/Header.tsx +2 -4
- package/corpus/templates/design/app/components/ui/tabs.tsx +59 -1
- package/corpus/templates/design/app/global.css +1 -34
- package/corpus/templates/design/app/i18n/en-US.ts +4 -0
- package/corpus/templates/design/app/lib/design-filter.ts +4 -2
- package/corpus/templates/design/app/pages/Index.tsx +91 -59
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +58 -17
- package/corpus/templates/design/changelog/2026-10-01-filter-designs-by-owner.md +6 -0
- package/corpus/templates/design/changelog/2026-10-03-layers-dropped-into-visible-canvas-space-beyond-a-screen-now.md +6 -0
- package/corpus/templates/design/changelog/2026-10-03-visible-screen-overflow-no-longer-routes-drops-to-the-canvas.md +6 -0
- package/corpus/templates/design/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-designs.md +6 -0
- package/corpus/templates/design/package.json +1 -0
- package/corpus/templates/design/server/plugins/auth.ts +2 -1
- package/corpus/templates/design/server/plugins/otel.ts +6 -0
- package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/dispatch/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/dispatch/README.md +2 -2
- package/corpus/templates/dispatch/package.json +1 -0
- package/corpus/templates/dispatch/server/plugins/otel.ts +6 -0
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +2 -1
- package/corpus/templates/factory/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/factory/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/factory/package.json +1 -0
- package/corpus/templates/factory/server/plugins/otel.ts +6 -0
- package/corpus/templates/forms/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/forms/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/forms/README.md +2 -2
- package/corpus/templates/forms/package.json +1 -0
- package/corpus/templates/forms/server/plugins/auth.ts +2 -1
- package/corpus/templates/forms/server/plugins/otel.ts +6 -0
- package/corpus/templates/mail/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/mail/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/mail/CHANGELOG.md +6 -0
- package/corpus/templates/mail/README.md +2 -2
- package/corpus/templates/mail/package.json +1 -0
- package/corpus/templates/mail/server/plugins/auth.ts +2 -1
- package/corpus/templates/mail/server/plugins/otel.ts +6 -0
- package/corpus/templates/plan/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/plan/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/plan/CHANGELOG.md +1 -0
- package/corpus/templates/plan/README.md +2 -2
- package/corpus/templates/plan/package.json +1 -0
- package/corpus/templates/plan/server/plugins/auth.ts +2 -1
- package/corpus/templates/plan/server/plugins/otel.ts +6 -0
- package/corpus/templates/slides/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/slides/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/slides/CHANGELOG.md +23 -0
- package/corpus/templates/slides/README.md +2 -2
- package/corpus/templates/slides/actions/list-decks.ts +11 -4
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +44 -36
- package/corpus/templates/slides/app/components/home/SlidesHomeLibrary.tsx +79 -0
- package/corpus/templates/slides/app/components/layout/Header.tsx +2 -15
- package/corpus/templates/slides/app/components/ui/tabs.tsx +59 -0
- package/corpus/templates/slides/app/global.css +3 -54
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/lib/deck-filter.ts +5 -2
- package/corpus/templates/slides/app/pages/Index.tsx +102 -88
- package/corpus/templates/slides/changelog/2026-10-01-filter-decks-by-owner.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-02-hide-recent-until-decks-available.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-decks-a.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +8 -0
- package/corpus/templates/slides/server/plugins/auth.ts +2 -1
- package/corpus/templates/slides/server/plugins/otel.ts +6 -0
- package/corpus/templates/tasks/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/tasks/.agents/skills/storing-data/SKILL.md +12 -7
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -290,9 +290,16 @@ export default defineEventHandler(async (event) => {
|
|
|
290
290
|
|
|
291
291
|
## Data Scoping
|
|
292
292
|
|
|
293
|
-
In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views
|
|
293
|
+
In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views, and checks every statement before it runs so it can only reach those views.
|
|
294
294
|
|
|
295
|
-
The `db-query` / `db-exec` tools (and the extension SQL bridge, which shares the same path)
|
|
295
|
+
The `db-query` / `db-exec` / `db-patch` tools (and the extension SQL bridge, which shares the same path) read each statement the way Postgres does and refuse anything that could reach data around the temp views:
|
|
296
|
+
|
|
297
|
+
- schema- or database-qualified names like `public.<table>`, which resolve to the base table;
|
|
298
|
+
- names that resolve to anything other than the current user's temp views, such as materialized views or tables the scoping does not cover;
|
|
299
|
+
- app-defined SQL or PL/pgSQL functions and operators, whose bodies read tables without the views;
|
|
300
|
+
- built-ins that run SQL text, change session settings, or reach outside the database (`query_to_xml`, `set_config`, `dblink`, most `pg_*` functions).
|
|
301
|
+
|
|
302
|
+
`db-query` runs in a read-only transaction, and an `INSERT` must list its columns when the table has access-control columns. Use bare table names and plain expressions; scoping is applied automatically. When the agent needs data the views do not expose, add an app action with its own access checks.
|
|
296
303
|
|
|
297
304
|
### Per-User Scoping (`owner_email`)
|
|
298
305
|
|
|
@@ -88,10 +88,20 @@ agent answers about browser recordings in the Analytics template.
|
|
|
88
88
|
The catalog keeps the 1,000 most recently seen events, sorted by volume, and
|
|
89
89
|
sets `truncated` when it cut the list; its app flags still count every event
|
|
90
90
|
in the range.
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
- Indexes hold caller text raw, and one entry over Postgres's limit fails the
|
|
92
|
+
whole batch. Ingest cuts every indexed value with
|
|
93
|
+
`server/lib/indexed-text.ts` (event name, app, template, path) before any
|
|
94
|
+
table sees it. A long user key keeps a prefix plus a hash of the whole value
|
|
95
|
+
(`boundedIdentity`), so two users never merge into one. Row ids built from
|
|
96
|
+
caller text go through `indexedRowId`, which hashes an id that
|
|
97
|
+
percent-encoding made too long. Session and recording ids are never cut,
|
|
98
|
+
because a cut id could merge two sessions: replay ingest rejects session and
|
|
99
|
+
recording ids over 256 characters, and an event's longer session id skips the
|
|
100
|
+
session index. Bound any new indexed caller value there.
|
|
101
|
+
- Public ingest (`/track`, `/api/analytics/replay`) returns a thrown message
|
|
102
|
+
only for an error built with `requestError` (a numeric `statusCode`). Any
|
|
103
|
+
other failure is logged and answered with a generic 500, because its message
|
|
104
|
+
can quote internal database details.
|
|
95
105
|
- Event filters exclude a session if any of its recordings started before the
|
|
96
106
|
tenant's coverage start, because one analytics session can span tabs.
|
|
97
107
|
Coverage starts only after a session write succeeds, and the reported start
|
|
@@ -75,10 +75,11 @@ state their PostgreSQL types directly.
|
|
|
75
75
|
|
|
76
76
|
Member offboarding and email changes refuse to run while any column named
|
|
77
77
|
`email`, `*_email`, `*scope_id`, `created_by`, `updated_by`, `invited_by`,
|
|
78
|
-
`owner`, `principal_id`, `session_id`, or `user_id` has no policy.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
`owner`, `principal_id`, `session_id`, or `user_id` has no policy.
|
|
79
|
+
`createSharesTable()` tables are handled for you, and undeclared `owner_email`
|
|
80
|
+
columns transfer to the successor, which is right only for owned content.
|
|
81
|
+
Declare every other one, including columns that are not member identities, from
|
|
82
|
+
the app's database plugin graph (Clips does it in `server/db/index.ts`):
|
|
82
83
|
|
|
83
84
|
```ts
|
|
84
85
|
import { registerIdentityColumns } from "@agent-native/core/org";
|
|
@@ -88,13 +89,17 @@ registerIdentityColumns([
|
|
|
88
89
|
{ table: "space_members", column: "email", emailChange: "rekey", offboard: "delete", orgScope: { column: "space_id", references: { table: "spaces", column: "id", orgColumn: "org_id" } }, reason: "Space membership grants access." },
|
|
89
90
|
// Someone else's address: never rewritten.
|
|
90
91
|
{ table: "meeting_participants", column: "email", emailChange: "retain", offboard: "retain", reason: "Attendee address from the calendar provider." },
|
|
92
|
+
// A credential in an owner_email table: never handed to the successor.
|
|
93
|
+
{ table: "api_keys", column: "owner_email", emailChange: "rekey", offboard: "revoke", reason: "Bearer keys act as their owner." },
|
|
91
94
|
]);
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
Choose `delete` for grants, credentials, and pending tokens
|
|
97
|
+
Choose `delete` for grants, credentials, and pending tokens, or `revoke` (sets
|
|
98
|
+
`revoked_at`) when a reader treats a missing row as "not revoked"; `retain` for
|
|
95
99
|
attribution, history, and third-party addresses; `transfer` only for owned
|
|
96
|
-
data.
|
|
97
|
-
|
|
100
|
+
data. Declare `owner_email` tables that hold credentials or grants, or
|
|
101
|
+
offboarding hands them to the successor. A table without `org_id` needs
|
|
102
|
+
`orgScope` or an organization-scoped removal leaves its rows alone.
|
|
98
103
|
|
|
99
104
|
| Template | Tables |
|
|
100
105
|
| ------------ | --------------------------------------------- |
|
|
@@ -7,8 +7,14 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
7
7
|
|
|
8
8
|
### Improved
|
|
9
9
|
|
|
10
|
+
- Builder.io setup now explains free credits and the existing-account option.
|
|
11
|
+
- Owners and admins can save data source credentials for the whole organization or just for themselves.
|
|
10
12
|
- Retention reports now compare returns from paid and untagged signups.
|
|
11
13
|
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Events with a very long name, app, or page path no longer cause the rest of their batch to be lost
|
|
17
|
+
|
|
12
18
|
## 2026-10-01
|
|
13
19
|
|
|
14
20
|
### Improved
|
|
@@ -4,7 +4,7 @@ An open-source, agent-native alternative to Amplitude and FullStory. Ask
|
|
|
4
4
|
analytics questions in plain English and get charts, dashboards, and re-runnable
|
|
5
5
|
investigations back — you own the code, the queries, and the data.
|
|
6
6
|
|
|
7
|
-
**Live app: [analytics.agent-native.com](https://analytics.agent-native.com)**
|
|
7
|
+
**Live app: [analytics.agent-native.com](https://analytics.agent-native.com?utm_source=github&utm_medium=referral&utm_content=analytics-readme)**
|
|
8
8
|
|
|
9
9
|
The agent connects to your data, writes and validates the SQL for you, and
|
|
10
10
|
renders the answer as a chart, table, metric, or saved dashboard panel. Anything
|
|
@@ -32,4 +32,4 @@ pnpm install
|
|
|
32
32
|
pnpm dev
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Full docs: [agent-native.com/docs/template-analytics](https://agent-native.com/docs/template-analytics).
|
|
35
|
+
Full docs: [agent-native.com/docs/template-analytics](https://agent-native.com/docs/template-analytics?utm_source=github&utm_medium=referral&utm_content=analytics-readme).
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
parseAnalyticsTrackPayload,
|
|
15
15
|
recordAnalyticsEvents,
|
|
16
16
|
} from "../lib/first-party-analytics.js";
|
|
17
|
+
import { errorReply } from "../lib/request-errors.js";
|
|
17
18
|
|
|
18
19
|
function setCors(event: any): void {
|
|
19
20
|
setResponseHeader(event, "Access-Control-Allow-Origin", "*");
|
|
@@ -39,18 +40,10 @@ export const handleAnalyticsTrack = defineEventHandler(async (event) => {
|
|
|
39
40
|
return { success: true, accepted: 0 };
|
|
40
41
|
}
|
|
41
42
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
body = { ...JSON.parse(body), publicKey: headerKey };
|
|
47
|
-
} else if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
48
|
-
body = { ...(body as Record<string, unknown>), publicKey: headerKey };
|
|
49
|
-
} else {
|
|
50
|
-
body = { publicKey: headerKey };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const parsed = parseAnalyticsTrackPayload(body);
|
|
43
|
+
const parsed = parseAnalyticsTrackPayload(
|
|
44
|
+
await readBody(event),
|
|
45
|
+
getHeader(event, "x-agent-native-analytics-key"),
|
|
46
|
+
);
|
|
54
47
|
const result = await recordAnalyticsEvents(parsed.publicKey, parsed.events);
|
|
55
48
|
setResponseStatus(event, 202);
|
|
56
49
|
return {
|
|
@@ -58,12 +51,9 @@ export const handleAnalyticsTrack = defineEventHandler(async (event) => {
|
|
|
58
51
|
accepted: result.accepted,
|
|
59
52
|
suppressedTestIdentity: result.suppressedTestIdentity,
|
|
60
53
|
};
|
|
61
|
-
} catch (err
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
typeof err?.statusCode === "number" ? err.statusCode : undefined;
|
|
66
|
-
setResponseStatus(event, invalidKey ? 401 : (statusCode ?? 400));
|
|
67
|
-
return { error: message };
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const reply = errorReply(err, "[first-party-analytics]");
|
|
56
|
+
setResponseStatus(event, reply.statusCode);
|
|
57
|
+
return { error: reply.error };
|
|
68
58
|
}
|
|
69
59
|
});
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
} from "h3";
|
|
16
16
|
|
|
17
17
|
import { runApiHandlerWithContext } from "../lib/credentials";
|
|
18
|
+
import {
|
|
19
|
+
errorReply,
|
|
20
|
+
parseJsonBody,
|
|
21
|
+
requestError,
|
|
22
|
+
} from "../lib/request-errors.js";
|
|
18
23
|
import {
|
|
19
24
|
resolveSessionReplayAgentAccess,
|
|
20
25
|
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
@@ -68,10 +73,6 @@ function setCors(event: any): void {
|
|
|
68
73
|
setResponseHeader(event, "Access-Control-Expose-Headers", "retry-after");
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
function statusFromError(error: any): number {
|
|
72
|
-
return typeof error?.statusCode === "number" ? error.statusCode : 400;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
76
|
function retryAfterFromError(error: any): number | null {
|
|
76
77
|
const seconds = error?.retryAfterSeconds;
|
|
77
78
|
return typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0
|
|
@@ -79,8 +80,10 @@ function retryAfterFromError(error: any): number | null {
|
|
|
79
80
|
: null;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function
|
|
83
|
-
|
|
83
|
+
function replyWithError(event: any, error: unknown): { error: string } {
|
|
84
|
+
const reply = errorReply(error, "[session-replay]");
|
|
85
|
+
setResponseStatus(event, reply.statusCode);
|
|
86
|
+
return { error: reply.error };
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
function hasQueryKey(query: Record<string, unknown>): boolean {
|
|
@@ -90,7 +93,7 @@ function hasQueryKey(query: Record<string, unknown>): boolean {
|
|
|
90
93
|
function injectHeaderKey(body: unknown, headerKey?: string): unknown {
|
|
91
94
|
if (!headerKey) return body;
|
|
92
95
|
if (typeof body === "string" && body.trim()) {
|
|
93
|
-
return { ...
|
|
96
|
+
return { ...(parseJsonBody(body) as object), publicKey: headerKey };
|
|
94
97
|
}
|
|
95
98
|
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
96
99
|
return { ...(body as Record<string, unknown>), publicKey: headerKey };
|
|
@@ -98,10 +101,6 @@ function injectHeaderKey(body: unknown, headerKey?: string): unknown {
|
|
|
98
101
|
return { publicKey: headerKey };
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
function statusError(message: string, statusCode: number): Error {
|
|
102
|
-
return Object.assign(new Error(message), { statusCode });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
104
|
function looksLikeDecodedJson(bytes: Buffer): boolean {
|
|
106
105
|
const first = bytes.toString("utf8").trimStart()[0];
|
|
107
106
|
return first === "{" || first === "[";
|
|
@@ -156,12 +155,12 @@ export function decodeSessionReplayRequestBody(
|
|
|
156
155
|
decoded = textWrappedGzip.decoded;
|
|
157
156
|
requestBytes = textWrappedGzip.requestBytes;
|
|
158
157
|
} else {
|
|
159
|
-
throw
|
|
158
|
+
throw requestError("Invalid gzip-compressed replay body", 400);
|
|
160
159
|
}
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
162
|
} else if (encoding && encoding !== "identity") {
|
|
164
|
-
throw
|
|
163
|
+
throw requestError(
|
|
165
164
|
`Unsupported replay request content-encoding: ${encoding}`,
|
|
166
165
|
415,
|
|
167
166
|
);
|
|
@@ -289,11 +288,9 @@ export const handleSessionReplayIngest = defineEventHandler(async (event) => {
|
|
|
289
288
|
try {
|
|
290
289
|
const query = getQuery(event);
|
|
291
290
|
if (hasQueryKey(query)) {
|
|
292
|
-
throw
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
),
|
|
296
|
-
{ statusCode: 400 },
|
|
291
|
+
throw requestError(
|
|
292
|
+
"Analytics public keys must be sent in the request body or x-agent-native-analytics-key header, not the query string",
|
|
293
|
+
400,
|
|
297
294
|
);
|
|
298
295
|
}
|
|
299
296
|
|
|
@@ -313,8 +310,7 @@ export const handleSessionReplayIngest = defineEventHandler(async (event) => {
|
|
|
313
310
|
if (retryAfter !== null) {
|
|
314
311
|
setResponseHeader(event, "Retry-After", String(retryAfter));
|
|
315
312
|
}
|
|
316
|
-
|
|
317
|
-
return { error: messageFromError(error) };
|
|
313
|
+
return replyWithError(event, error);
|
|
318
314
|
}
|
|
319
315
|
});
|
|
320
316
|
|
|
@@ -326,9 +322,8 @@ export const handleSessionReplayList = defineEventHandler(async (event) => {
|
|
|
326
322
|
listFiltersFromQuery(getQuery(event)),
|
|
327
323
|
);
|
|
328
324
|
return { recordings };
|
|
329
|
-
} catch (error
|
|
330
|
-
|
|
331
|
-
return { error: messageFromError(error) };
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return replyWithError(event, error);
|
|
332
327
|
}
|
|
333
328
|
});
|
|
334
329
|
});
|
|
@@ -347,9 +342,8 @@ export const handleSessionReplaySummary = defineEventHandler(async (event) => {
|
|
|
347
342
|
orgId: ctx.orgId ?? null,
|
|
348
343
|
});
|
|
349
344
|
return { recording };
|
|
350
|
-
} catch (error
|
|
351
|
-
|
|
352
|
-
return { error: messageFromError(error) };
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return replyWithError(event, error);
|
|
353
347
|
}
|
|
354
348
|
});
|
|
355
349
|
});
|
|
@@ -373,9 +367,8 @@ export const handleSessionReplayEvents = defineEventHandler(async (event) => {
|
|
|
373
367
|
limit: asInt(query.limit),
|
|
374
368
|
},
|
|
375
369
|
);
|
|
376
|
-
} catch (error
|
|
377
|
-
|
|
378
|
-
return { error: messageFromError(error) };
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return replyWithError(event, error);
|
|
379
372
|
}
|
|
380
373
|
});
|
|
381
374
|
});
|
|
@@ -409,9 +402,8 @@ export const handleSessionReplayManifest = defineEventHandler(async (event) => {
|
|
|
409
402
|
),
|
|
410
403
|
})),
|
|
411
404
|
};
|
|
412
|
-
} catch (error
|
|
413
|
-
|
|
414
|
-
return { error: messageFromError(error) };
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return replyWithError(event, error);
|
|
415
407
|
}
|
|
416
408
|
}
|
|
417
409
|
|
|
@@ -421,9 +413,8 @@ export const handleSessionReplayManifest = defineEventHandler(async (event) => {
|
|
|
421
413
|
userEmail: ctx.userEmail,
|
|
422
414
|
orgId: ctx.orgId ?? null,
|
|
423
415
|
});
|
|
424
|
-
} catch (error
|
|
425
|
-
|
|
426
|
-
return { error: messageFromError(error) };
|
|
416
|
+
} catch (error) {
|
|
417
|
+
return replyWithError(event, error);
|
|
427
418
|
}
|
|
428
419
|
});
|
|
429
420
|
});
|
|
@@ -454,9 +445,8 @@ export const handleSessionReplayChunkBytes = defineEventHandler(
|
|
|
454
445
|
setResponseHeader(event, "X-Session-Replay-Seq", String(result.seq));
|
|
455
446
|
setResponseHeader(event, "X-Session-Replay-Checksum", result.checksum);
|
|
456
447
|
return result.json;
|
|
457
|
-
} catch (error
|
|
458
|
-
|
|
459
|
-
return { error: messageFromError(error) };
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return replyWithError(event, error);
|
|
460
450
|
}
|
|
461
451
|
}
|
|
462
452
|
|
|
@@ -471,9 +461,8 @@ export const handleSessionReplayChunkBytes = defineEventHandler(
|
|
|
471
461
|
setResponseHeader(event, "X-Session-Replay-Seq", String(result.seq));
|
|
472
462
|
setResponseHeader(event, "X-Session-Replay-Checksum", result.checksum);
|
|
473
463
|
return result.json;
|
|
474
|
-
} catch (error
|
|
475
|
-
|
|
476
|
-
return { error: messageFromError(error) };
|
|
464
|
+
} catch (error) {
|
|
465
|
+
return replyWithError(event, error);
|
|
477
466
|
}
|
|
478
467
|
});
|
|
479
468
|
},
|
|
@@ -502,9 +491,8 @@ export const handleSessionReplayChunkBatch = defineEventHandler(
|
|
|
502
491
|
applyAgentReplayReadHeaders(event);
|
|
503
492
|
setResponseHeader(event, "Content-Type", "application/json");
|
|
504
493
|
return result;
|
|
505
|
-
} catch (error
|
|
506
|
-
|
|
507
|
-
return { error: messageFromError(error) };
|
|
494
|
+
} catch (error) {
|
|
495
|
+
return replyWithError(event, error);
|
|
508
496
|
}
|
|
509
497
|
}
|
|
510
498
|
|
|
@@ -517,9 +505,8 @@ export const handleSessionReplayChunkBatch = defineEventHandler(
|
|
|
517
505
|
setResponseHeader(event, "Content-Type", "application/json");
|
|
518
506
|
setResponseHeader(event, "Cache-Control", "no-store");
|
|
519
507
|
return result;
|
|
520
|
-
} catch (error
|
|
521
|
-
|
|
522
|
-
return { error: messageFromError(error) };
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return replyWithError(event, error);
|
|
523
510
|
}
|
|
524
511
|
});
|
|
525
512
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sql } from "@agent-native/core/db/schema";
|
|
2
2
|
|
|
3
3
|
import { getDb, schema } from "../db/index.js";
|
|
4
|
+
import { indexedRowId } from "./indexed-text.js";
|
|
4
5
|
|
|
5
6
|
export interface NormalizedFirstPartyAnalyticsEventRow {
|
|
6
7
|
eventName: string;
|
|
@@ -63,10 +64,6 @@ function compositeKey(parts: readonly string[]): string {
|
|
|
63
64
|
return JSON.stringify(parts);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function stableId(prefix: string, parts: readonly string[]): string {
|
|
67
|
-
return `${prefix}_${parts.map((part) => encodeURIComponent(part)).join("|")}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
67
|
export async function upsertFirstPartyAnalyticsRollups(
|
|
71
68
|
rows: readonly NormalizedFirstPartyAnalyticsEventRow[],
|
|
72
69
|
transaction?: any,
|
|
@@ -96,7 +93,13 @@ export async function upsertFirstPartyAnalyticsRollups(
|
|
|
96
93
|
existingDaily.eventCount += 1;
|
|
97
94
|
} else {
|
|
98
95
|
dailyRollups.set(dailyKey, {
|
|
99
|
-
id:
|
|
96
|
+
id: indexedRowId("aedr", [
|
|
97
|
+
scopeKey,
|
|
98
|
+
eventDate,
|
|
99
|
+
eventName,
|
|
100
|
+
app,
|
|
101
|
+
template,
|
|
102
|
+
]),
|
|
100
103
|
tenantKey: scopeKey,
|
|
101
104
|
ownerEmail,
|
|
102
105
|
orgId,
|
|
@@ -112,7 +115,7 @@ export async function upsertFirstPartyAnalyticsRollups(
|
|
|
112
115
|
const userDayKey = compositeKey([scopeKey, eventDate, userKey]);
|
|
113
116
|
if (!userDays.has(userDayKey)) {
|
|
114
117
|
userDays.set(userDayKey, {
|
|
115
|
-
id:
|
|
118
|
+
id: indexedRowId("aud", [scopeKey, eventDate, userKey]),
|
|
116
119
|
tenantKey: scopeKey,
|
|
117
120
|
ownerEmail,
|
|
118
121
|
orgId,
|
|
@@ -39,6 +39,15 @@ import {
|
|
|
39
39
|
} from "./first-party-analytics-health.js";
|
|
40
40
|
import { upsertFirstPartyAnalyticsRollups } from "./first-party-analytics-rollups.js";
|
|
41
41
|
import { reserveFirstPartyPostgresEventVolume } from "./first-party-analytics-volume.js";
|
|
42
|
+
import {
|
|
43
|
+
MAX_APP_LENGTH,
|
|
44
|
+
MAX_EVENT_NAME_LENGTH,
|
|
45
|
+
MAX_PATH_LENGTH,
|
|
46
|
+
MAX_USER_KEY_LENGTH,
|
|
47
|
+
boundedIdentity,
|
|
48
|
+
boundedText,
|
|
49
|
+
} from "./indexed-text.js";
|
|
50
|
+
import { parseIngestBody, requestError } from "./request-errors.js";
|
|
42
51
|
import {
|
|
43
52
|
recordEventCatalog,
|
|
44
53
|
recordSessionEventIndex,
|
|
@@ -488,14 +497,9 @@ function asRecord(value: unknown): Record<string, unknown> {
|
|
|
488
497
|
: {};
|
|
489
498
|
}
|
|
490
499
|
|
|
491
|
-
const LONE_SURROGATE =
|
|
492
|
-
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
493
|
-
|
|
494
|
-
// Rollup and index ids encode these values with encodeURIComponent, which
|
|
495
|
-
// throws on a lone surrogate and would reject the batch on every retry.
|
|
496
500
|
function asString(value: unknown): string | null {
|
|
497
501
|
if (typeof value === "string" && value.trim()) {
|
|
498
|
-
return value.trim()
|
|
502
|
+
return value.trim();
|
|
499
503
|
}
|
|
500
504
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
501
505
|
return String(value);
|
|
@@ -564,29 +568,35 @@ export function resolveAnalyticsEventDimensions({
|
|
|
564
568
|
context: Record<string, unknown>;
|
|
565
569
|
hostname: string | null;
|
|
566
570
|
}): { app: string | null; template: string | null } {
|
|
567
|
-
const app =
|
|
571
|
+
const app = boundedDimension(
|
|
568
572
|
asString(properties.app_name) ||
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
573
|
+
asString(properties.app) ||
|
|
574
|
+
asString((properties as any).agent_native_app) ||
|
|
575
|
+
asString((properties as any).agentNativeApp) ||
|
|
576
|
+
asString((context as any).app) ||
|
|
577
|
+
asString((context as any).agent_native_app) ||
|
|
578
|
+
asString((context as any).agentNativeApp) ||
|
|
579
|
+
(hostname ? hostname.split(".")[0] : null),
|
|
580
|
+
);
|
|
576
581
|
const template =
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
582
|
+
boundedDimension(
|
|
583
|
+
asString(properties.template_name) ||
|
|
584
|
+
asString(properties.template) ||
|
|
585
|
+
asString((properties as any).templateId) ||
|
|
586
|
+
asString((properties as any).agent_native_template) ||
|
|
587
|
+
asString((properties as any).agentNativeTemplate) ||
|
|
588
|
+
asString((context as any).template) ||
|
|
589
|
+
asString((context as any).templateId) ||
|
|
590
|
+
asString((context as any).agent_native_template) ||
|
|
591
|
+
asString((context as any).agentNativeTemplate),
|
|
592
|
+
) || app;
|
|
587
593
|
return { app, template };
|
|
588
594
|
}
|
|
589
595
|
|
|
596
|
+
function boundedDimension(value: string | null): string | null {
|
|
597
|
+
return value && boundedText(value, MAX_APP_LENGTH);
|
|
598
|
+
}
|
|
599
|
+
|
|
590
600
|
export function isMarketingWebsiteSessionEvent({
|
|
591
601
|
eventName,
|
|
592
602
|
hostname,
|
|
@@ -616,35 +626,43 @@ export function isMarketingWebsiteSessionEvent({
|
|
|
616
626
|
);
|
|
617
627
|
}
|
|
618
628
|
|
|
619
|
-
export function parseAnalyticsTrackPayload(
|
|
629
|
+
export function parseAnalyticsTrackPayload(
|
|
630
|
+
raw: unknown,
|
|
631
|
+
headerKey?: string | null,
|
|
632
|
+
): {
|
|
620
633
|
publicKey: string;
|
|
621
634
|
events: IncomingAnalyticsEvent[];
|
|
622
635
|
} {
|
|
623
|
-
const body =
|
|
624
|
-
typeof raw === "string" && raw.trim() ? JSON.parse(raw) : asRecord(raw);
|
|
636
|
+
const body = asRecord(parseIngestBody(raw));
|
|
625
637
|
const publicKey =
|
|
638
|
+
asString(headerKey) ||
|
|
626
639
|
asString((body as any).publicKey) ||
|
|
627
640
|
asString((body as any).writeKey) ||
|
|
628
641
|
asString((body as any).apiKey);
|
|
629
642
|
if (!publicKey) {
|
|
630
|
-
throw
|
|
643
|
+
throw requestError("Missing publicKey", 400);
|
|
631
644
|
}
|
|
632
645
|
|
|
633
646
|
const rawEvents = Array.isArray((body as any).events)
|
|
634
647
|
? (body as any).events
|
|
635
648
|
: [body];
|
|
636
649
|
if (rawEvents.length === 0) {
|
|
637
|
-
throw
|
|
650
|
+
throw requestError("No events provided", 400);
|
|
638
651
|
}
|
|
639
652
|
if (rawEvents.length > MAX_EVENTS_PER_REQUEST) {
|
|
640
|
-
throw
|
|
653
|
+
throw requestError(
|
|
654
|
+
`At most ${MAX_EVENTS_PER_REQUEST} events are accepted`,
|
|
655
|
+
400,
|
|
656
|
+
);
|
|
641
657
|
}
|
|
642
658
|
|
|
643
659
|
const events = rawEvents.map((rawEvent: unknown) => {
|
|
644
660
|
const obj = asRecord(rawEvent);
|
|
645
661
|
const eventName =
|
|
646
662
|
asString((obj as any).event) || asString((obj as any).name);
|
|
647
|
-
if (!eventName)
|
|
663
|
+
if (!eventName) {
|
|
664
|
+
throw requestError("Each event requires an event name", 400);
|
|
665
|
+
}
|
|
648
666
|
return {
|
|
649
667
|
event: eventName,
|
|
650
668
|
properties: asRecord((obj as any).properties),
|
|
@@ -713,7 +731,7 @@ export async function recordAnalyticsEvents(
|
|
|
713
731
|
)
|
|
714
732
|
.limit(1);
|
|
715
733
|
if (!key) {
|
|
716
|
-
throw
|
|
734
|
+
throw requestError("Invalid analytics public key", 401);
|
|
717
735
|
}
|
|
718
736
|
|
|
719
737
|
const receivedAt = nowIso();
|
|
@@ -752,7 +770,9 @@ export async function recordAnalyticsEvents(
|
|
|
752
770
|
event.anonymousId ??
|
|
753
771
|
asString((properties as any).anonymousId) ??
|
|
754
772
|
asString((properties as any).distinctId);
|
|
755
|
-
const
|
|
773
|
+
const rawUserKey = userId || anonymousId;
|
|
774
|
+
const userKey =
|
|
775
|
+
rawUserKey && boundedIdentity(rawUserKey, MAX_USER_KEY_LENGTH);
|
|
756
776
|
const timestamp = normalizeAnalyticsTimestamp(event.timestamp, receivedAt);
|
|
757
777
|
const sessionId =
|
|
758
778
|
event.sessionId ??
|
|
@@ -775,7 +795,7 @@ export async function recordAnalyticsEvents(
|
|
|
775
795
|
app,
|
|
776
796
|
template,
|
|
777
797
|
url: parts.url,
|
|
778
|
-
userId,
|
|
798
|
+
userId: userId && boundedIdentity(userId, MAX_USER_KEY_LENGTH),
|
|
779
799
|
anonymousId,
|
|
780
800
|
userKey,
|
|
781
801
|
sessionId,
|
|
@@ -789,10 +809,11 @@ export async function recordAnalyticsEvents(
|
|
|
789
809
|
return [];
|
|
790
810
|
}
|
|
791
811
|
|
|
812
|
+
const path = parts.path ?? asString(properties.path);
|
|
792
813
|
return {
|
|
793
814
|
id: id("evt"),
|
|
794
815
|
publicKeyId: key.id,
|
|
795
|
-
eventName: event.event,
|
|
816
|
+
eventName: boundedText(event.event, MAX_EVENT_NAME_LENGTH),
|
|
796
817
|
userId,
|
|
797
818
|
anonymousId,
|
|
798
819
|
userKey,
|
|
@@ -801,7 +822,7 @@ export async function recordAnalyticsEvents(
|
|
|
801
822
|
eventDate: eventDateFromTimestamp(timestamp),
|
|
802
823
|
receivedAt,
|
|
803
824
|
url: parts.url,
|
|
804
|
-
path:
|
|
825
|
+
path: path && boundedText(path, MAX_PATH_LENGTH),
|
|
805
826
|
hostname,
|
|
806
827
|
referrer:
|
|
807
828
|
asString(properties.referrer) || asString((context as any).referrer),
|