@vibes.diy/api-svc 4.5.5 → 5.2.2
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/autumn.config.d.ts +4 -2
- package/autumn.config.js +12 -7
- package/autumn.config.js.map +1 -1
- package/billing.d.ts +77 -1
- package/billing.js +264 -3
- package/billing.js.map +1 -1
- package/cf-serve.d.ts +4 -1
- package/cf-serve.js +23 -0
- package/cf-serve.js.map +1 -1
- package/create-handler.d.ts +9 -1
- package/create-handler.js +5 -0
- package/create-handler.js.map +1 -1
- package/evento-handler-manifest.js +36 -0
- package/evento-handler-manifest.js.map +1 -1
- package/index.d.ts +17 -0
- package/index.js +17 -0
- package/index.js.map +1 -1
- package/intern/backend-db-callback.js +56 -0
- package/intern/backend-db-callback.js.map +1 -1
- package/intern/backend-egress.d.ts +1 -1
- package/intern/backend-egress.js +74 -22
- package/intern/backend-egress.js.map +1 -1
- package/intern/backend-policy-version.d.ts +1 -1
- package/intern/backend-policy-version.js +1 -1
- package/intern/components/vibe-page.js +5 -1
- package/intern/components/vibe-page.js.map +1 -1
- package/intern/delete-access-gate.d.ts +4 -0
- package/intern/delete-access-gate.js +46 -5
- package/intern/delete-access-gate.js.map +1 -1
- package/intern/egress-platform-list.d.ts +1 -0
- package/intern/egress-platform-list.js +4 -0
- package/intern/egress-platform-list.js.map +1 -1
- package/intern/emit-notification.d.ts +21 -0
- package/intern/emit-notification.js +43 -0
- package/intern/emit-notification.js.map +1 -0
- package/intern/ensure-app-metadata.js +4 -0
- package/intern/ensure-app-metadata.js.map +1 -1
- package/intern/ensure-chat-id.js +13 -2
- package/intern/ensure-chat-id.js.map +1 -1
- package/intern/ensure-slug-binding.js +20 -2
- package/intern/ensure-slug-binding.js.map +1 -1
- package/intern/notify-comment-posted.d.ts +8 -0
- package/intern/notify-comment-posted.js +24 -0
- package/intern/notify-comment-posted.js.map +1 -0
- package/intern/notify-dm-received.d.ts +9 -0
- package/intern/notify-dm-received.js +25 -0
- package/intern/notify-dm-received.js.map +1 -0
- package/intern/notify-member-joined.d.ts +11 -0
- package/intern/notify-member-joined.js +16 -0
- package/intern/notify-member-joined.js.map +1 -0
- package/intern/notify-remix-clone.d.ts +6 -0
- package/intern/notify-remix-clone.js +21 -0
- package/intern/notify-remix-clone.js.map +1 -0
- package/intern/notify-remix.d.ts +8 -0
- package/intern/notify-remix.js +23 -0
- package/intern/notify-remix.js.map +1 -0
- package/intern/notify-request-grant.d.ts +9 -0
- package/intern/notify-request-grant.js +18 -0
- package/intern/notify-request-grant.js.map +1 -0
- package/intern/notify-social-edge.d.ts +9 -0
- package/intern/notify-social-edge.js +45 -0
- package/intern/notify-social-edge.js.map +1 -0
- package/intern/notify-vibe-published.d.ts +7 -0
- package/intern/notify-vibe-published.js +22 -0
- package/intern/notify-vibe-published.js.map +1 -0
- package/intern/pre-allocate.d.ts +1 -0
- package/intern/pre-allocate.js +1 -0
- package/intern/pre-allocate.js.map +1 -1
- package/intern/process-access-bindings.js +12 -2
- package/intern/process-access-bindings.js.map +1 -1
- package/intern/put-access-gate.js +25 -3
- package/intern/put-access-gate.js.map +1 -1
- package/intern/read-anonymous-seed-docs.js +1 -1
- package/intern/read-anonymous-seed-docs.js.map +1 -1
- package/intern/recovery.d.ts +7 -1
- package/intern/recovery.js +5 -5
- package/intern/recovery.js.map +1 -1
- package/intern/render-vibe.js +7 -3
- package/intern/render-vibe.js.map +1 -1
- package/intern/slot-assembler.d.ts +2 -0
- package/intern/slot-assembler.js +9 -3
- package/intern/slot-assembler.js.map +1 -1
- package/intern/vibe-curation-decision.d.ts +18 -0
- package/intern/vibe-curation-decision.js +46 -0
- package/intern/vibe-curation-decision.js.map +1 -0
- package/intern/write-apps.js +9 -2
- package/intern/write-apps.js.map +1 -1
- package/package.json +16 -15
- package/public/access-function.js +4 -1
- package/public/access-function.js.map +1 -1
- package/public/access-helpers.js +5 -0
- package/public/access-helpers.js.map +1 -1
- package/public/app-documents-broadcast.d.ts +8 -0
- package/public/app-documents-broadcast.js +15 -0
- package/public/app-documents-broadcast.js.map +1 -1
- package/public/app-documents-grant-delta.js +3 -2
- package/public/app-documents-grant-delta.js.map +1 -1
- package/public/app-documents-read-eventos.js +72 -13
- package/public/app-documents-read-eventos.js.map +1 -1
- package/public/app-documents-write-eventos.js +28 -11
- package/public/app-documents-write-eventos.js.map +1 -1
- package/public/app-settings-sql-upsert.js +4 -1
- package/public/app-settings-sql-upsert.js.map +1 -1
- package/public/audience-arms-store.d.ts +21 -0
- package/public/audience-arms-store.js +110 -0
- package/public/audience-arms-store.js.map +1 -0
- package/public/audience-channels.d.ts +14 -0
- package/public/audience-channels.js +71 -0
- package/public/audience-channels.js.map +1 -0
- package/public/audience-derived-trigger.d.ts +15 -0
- package/public/audience-derived-trigger.js +45 -0
- package/public/audience-derived-trigger.js.map +1 -0
- package/public/audience-key-patch.d.ts +16 -0
- package/public/audience-key-patch.js +38 -0
- package/public/audience-key-patch.js.map +1 -0
- package/public/billing-msg.d.ts +5 -0
- package/public/billing-msg.js +143 -0
- package/public/billing-msg.js.map +1 -0
- package/public/channel-read-filter.js +4 -2
- package/public/channel-read-filter.js.map +1 -1
- package/public/curated-new-vibes.d.ts +5 -0
- package/public/curated-new-vibes.js +64 -0
- package/public/curated-new-vibes.js.map +1 -0
- package/public/developer-grants-flow.d.ts +5 -0
- package/public/developer-grants-flow.js +218 -0
- package/public/developer-grants-flow.js.map +1 -0
- package/public/developer-grants.d.ts +12 -0
- package/public/developer-grants.js +40 -0
- package/public/developer-grants.js.map +1 -0
- package/public/email-unsubscribe.d.ts +7 -0
- package/public/email-unsubscribe.js +41 -0
- package/public/email-unsubscribe.js.map +1 -0
- package/public/ensure-app-settings.js +69 -3
- package/public/ensure-app-settings.js.map +1 -1
- package/public/ensure-user-settings.js +9 -1
- package/public/ensure-user-settings.js.map +1 -1
- package/public/fork-app.js +24 -1
- package/public/fork-app.js.map +1 -1
- package/public/grant-reduce.d.ts +4 -2
- package/public/grant-reduce.js +11 -3
- package/public/grant-reduce.js.map +1 -1
- package/public/home-feed-common.d.ts +18 -0
- package/public/home-feed-common.js +46 -0
- package/public/home-feed-common.js.map +1 -0
- package/public/home-leaderboard.d.ts +5 -0
- package/public/home-leaderboard.js +64 -0
- package/public/home-leaderboard.js.map +1 -0
- package/public/image-gen.js +9 -0
- package/public/image-gen.js.map +1 -1
- package/public/invite-unsubscribe.d.ts +3 -0
- package/public/invite-unsubscribe.js +31 -0
- package/public/invite-unsubscribe.js.map +1 -0
- package/public/list-notifications.d.ts +4 -0
- package/public/list-notifications.js +157 -0
- package/public/list-notifications.js.map +1 -0
- package/public/related-vibes.d.ts +3 -0
- package/public/related-vibes.js +134 -0
- package/public/related-vibes.js.map +1 -0
- package/public/serv-entry-point.js +2 -2
- package/public/serv-entry-point.js.map +1 -1
- package/public/social-flow.d.ts +21 -0
- package/public/social-flow.js +765 -0
- package/public/social-flow.js.map +1 -0
- package/public/social-invite-flow.d.ts +3 -0
- package/public/social-invite-flow.js +73 -0
- package/public/social-invite-flow.js.map +1 -0
- package/public/social-invite-signup-match.d.ts +6 -0
- package/public/social-invite-signup-match.js +38 -0
- package/public/social-invite-signup-match.js.map +1 -0
- package/public/social-invite-store.d.ts +70 -0
- package/public/social-invite-store.js +172 -0
- package/public/social-invite-store.js.map +1 -0
- package/public/social-store.d.ts +109 -0
- package/public/social-store.js +469 -0
- package/public/social-store.js.map +1 -0
- package/public/who-am-i.js +5 -3
- package/public/who-am-i.js.map +1 -1
- package/svc-ws-send-provider.d.ts +2 -0
- package/svc-ws-send-provider.js +2 -0
- package/svc-ws-send-provider.js.map +1 -1
- package/types.d.ts +20 -1
- package/types.js.map +1 -1
- package/usage-report/README.md +1 -0
- package/usage-report/billing-reconcile-util.d.ts +7 -5
- package/usage-report/billing-reconcile-util.js +32 -17
- package/usage-report/billing-reconcile-util.js.map +1 -1
- package/usage-report/vibe-usage.py +171 -0
package/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { SuperThis, FPApiToken, DeviceIdCAIf } from "@vibes.diy/identity";
|
|
|
2
2
|
import { WSSendProvider } from "./svc-ws-send-provider.js";
|
|
3
3
|
import { Logger, Result } from "@adviser/cement";
|
|
4
4
|
import { LLMRequest } from "@vibes.diy/call-ai-v2";
|
|
5
|
-
import { type AccessDescriptor, type UserContext, type EvtViewerGrantsChanged, type EvtRequestGrant, type EvtUserNotification, LLMHeaders, MsgBase, VibesAssetStorage, VibesFPApiParameters } from "@vibes.diy/api-types";
|
|
5
|
+
import { type AccessDescriptor, type UserContext, type EvtViewerGrantsChanged, type EvtRequestGrant, type EvtSocialEdge, type EvtUserNotification, LLMHeaders, MsgBase, VibesAssetStorage, VibesFPApiParameters } from "@vibes.diy/api-types";
|
|
6
6
|
import { DBFlavour, VibesApiTables, VibesSqlite } from "@vibes.diy/api-sql";
|
|
7
7
|
import { type } from "arktype";
|
|
8
8
|
import type { AssetGrantSigner } from "./asset-grant.js";
|
|
@@ -55,6 +55,13 @@ export interface VibesApiSQLCtx {
|
|
|
55
55
|
docId: string;
|
|
56
56
|
channel?: string;
|
|
57
57
|
}, senderConnId: string): Promise<void>;
|
|
58
|
+
notifyDocChangedRemote?(evt: {
|
|
59
|
+
ownerHandle: string;
|
|
60
|
+
appSlug: string;
|
|
61
|
+
dbName: string;
|
|
62
|
+
docId: string;
|
|
63
|
+
channels: string[];
|
|
64
|
+
}): Promise<void>;
|
|
58
65
|
registerDocSubscription?(subscriptionKey: string): Promise<void>;
|
|
59
66
|
deregisterDocSubscription?(subscriptionKey: string): Promise<void>;
|
|
60
67
|
notifyDocEphemeral?(evt: {
|
|
@@ -69,12 +76,24 @@ export interface VibesApiSQLCtx {
|
|
|
69
76
|
notifyRequestGrantChanged?(evt: EvtRequestGrant, senderConnId: string): Promise<void>;
|
|
70
77
|
registerRequestGrantSubscription?(subscriptionKey: string): Promise<void>;
|
|
71
78
|
deregisterRequestGrantSubscription?(subscriptionKey: string): Promise<void>;
|
|
79
|
+
notifySocialEdgeChanged?(evt: EvtSocialEdge, senderConnId: string): Promise<void>;
|
|
80
|
+
registerSocialSubscription?(subscriptionKey: string): Promise<void>;
|
|
81
|
+
deregisterSocialSubscription?(subscriptionKey: string): Promise<void>;
|
|
72
82
|
notifyViewerGrantsChanged?(evt: EvtViewerGrantsChanged, senderConnId: string): Promise<void>;
|
|
73
83
|
registerViewerGrantsSubscription?(subscriptionKey: string): Promise<void>;
|
|
74
84
|
deregisterViewerGrantsSubscription?(subscriptionKey: string): Promise<void>;
|
|
75
85
|
notifyUser?(userId: string, evt: EvtUserNotification, senderConnId: string): Promise<void>;
|
|
76
86
|
registerUserSubscription?(userId: string): Promise<void>;
|
|
77
87
|
deregisterUserSubscription?(userId: string): Promise<void>;
|
|
88
|
+
registerAudienceReader?(handle: string): Promise<void>;
|
|
89
|
+
patchAudienceKeysRemote?(targetHandle: string, patch: {
|
|
90
|
+
op: "add" | "remove";
|
|
91
|
+
channels: string[];
|
|
92
|
+
appScope?: {
|
|
93
|
+
ownerHandle: string;
|
|
94
|
+
appSlug: string;
|
|
95
|
+
};
|
|
96
|
+
}): Promise<void>;
|
|
78
97
|
registerPushToken?(userId: string, token: string, platform: "ios", appVersion: string): Promise<void>;
|
|
79
98
|
deregisterPushToken?(userId: string, token: string): Promise<void>;
|
|
80
99
|
invokeAccessFn?(params: {
|
package/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../jsr/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../jsr/types.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AA0J/B,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC;IAChC,IAAI,EAAE,+BAA+B;IACrC,MAAM,EAAE,QAAQ;IAChB,WAAW,EAAE,QAAQ;IACrB,MAAM,EAAE,QAAQ;CACjB,CAAC,CAAC;AAGH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,OAAO,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,8BAA8B;IACpC,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,QAAQ;IACjB,MAAM,EAAE,QAAQ;CACjB,CAAC,CAAC;AAGH,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,OAAO,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;IACnC,IAAI,EAAE,mCAAmC;IACzC,WAAW,EAAE,aAAa;IAC1B,OAAO,EAAE,cAAc;CACxB,CAAC,CAAC;AAGH,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,OAAO,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,CAAC;AACzD,CAAC"}
|
package/usage-report/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Files:
|
|
|
7
7
|
- `inspect-db.ts`: read-only database inspector for connection info, table listings, table samples, and ad hoc SQL.
|
|
8
8
|
- `inspect-db-report.ts`: report generator that queries the database and produces the HTML report.
|
|
9
9
|
- `inspect-db-report-template.ts`: HTML template/rendering module used by the report generator.
|
|
10
|
+
- `vibe-usage.py`: cross-plane usage monitor for a deployed vibe (Postgres writers + Cloudflare visitors + Clerk sign-ups → one funnel). See `agents/vibe-usage-analytics.md`. Run: `python3 vibe-usage.py [HOURS]` (default 48); override `APP_SLUG` / `CF_PATH` env for other apps.
|
|
10
11
|
|
|
11
12
|
Commands:
|
|
12
13
|
|
|
@@ -9,17 +9,19 @@ export interface AutumnUsageRow {
|
|
|
9
9
|
export interface ReconciliationRow {
|
|
10
10
|
readonly userId: string;
|
|
11
11
|
readonly dbCents: number;
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
12
|
+
readonly expectedCredits: number;
|
|
13
|
+
readonly autumnCredits: number;
|
|
14
|
+
readonly driftCredits: number;
|
|
14
15
|
}
|
|
15
16
|
export interface ReconciliationReport {
|
|
16
17
|
readonly rows: readonly ReconciliationRow[];
|
|
17
18
|
readonly totalDbCents: number;
|
|
18
|
-
readonly
|
|
19
|
-
readonly
|
|
19
|
+
readonly totalExpectedCredits: number;
|
|
20
|
+
readonly totalAutumnCredits: number;
|
|
21
|
+
readonly totalDriftCredits: number;
|
|
20
22
|
readonly driftPct: number;
|
|
21
23
|
}
|
|
22
24
|
export declare function mergeReconciliation(db: readonly DbCostRow[], autumn: readonly AutumnUsageRow[]): ReconciliationReport;
|
|
23
|
-
export declare function usersOverThreshold(report: ReconciliationReport,
|
|
25
|
+
export declare function usersOverThreshold(report: ReconciliationReport, credits: number): number;
|
|
24
26
|
export declare function usagePercentile(report: ReconciliationReport, p: number): number;
|
|
25
27
|
export declare function renderReconciliationMarkdown(report: ReconciliationReport, thresholds: readonly number[]): string;
|
|
@@ -1,23 +1,33 @@
|
|
|
1
|
+
const DISPLAY_CREDITS_PER_CENT = 2;
|
|
1
2
|
export function mergeReconciliation(db, autumn) {
|
|
2
3
|
const ids = new Set([...db.map((r) => r.userId), ...autumn.map((r) => r.userId)]);
|
|
3
4
|
const dbBy = new Map(db.map((r) => [r.userId, r.cents]));
|
|
4
5
|
const autumnBy = new Map(autumn.map((r) => [r.userId, r.usage]));
|
|
5
6
|
const rows = [...ids].sort().map((userId) => {
|
|
6
7
|
const dbCents = dbBy.get(userId) ?? 0;
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const expectedCredits = dbCents * DISPLAY_CREDITS_PER_CENT;
|
|
9
|
+
const autumnCredits = autumnBy.get(userId) ?? 0;
|
|
10
|
+
return { userId, dbCents, expectedCredits, autumnCredits, driftCredits: expectedCredits - autumnCredits };
|
|
9
11
|
});
|
|
10
12
|
const totalDbCents = rows.reduce((s, r) => s + r.dbCents, 0);
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const totalExpectedCredits = rows.reduce((s, r) => s + r.expectedCredits, 0);
|
|
14
|
+
const totalAutumnCredits = rows.reduce((s, r) => s + r.autumnCredits, 0);
|
|
15
|
+
const totalDriftCredits = totalExpectedCredits - totalAutumnCredits;
|
|
16
|
+
const driftPct = totalExpectedCredits === 0 ? 0 : (Math.abs(totalDriftCredits) / totalExpectedCredits) * 100;
|
|
17
|
+
return {
|
|
18
|
+
rows,
|
|
19
|
+
totalDbCents,
|
|
20
|
+
totalExpectedCredits,
|
|
21
|
+
totalAutumnCredits,
|
|
22
|
+
totalDriftCredits,
|
|
23
|
+
driftPct,
|
|
24
|
+
};
|
|
15
25
|
}
|
|
16
|
-
export function usersOverThreshold(report,
|
|
17
|
-
return report.rows.filter((r) => r.
|
|
26
|
+
export function usersOverThreshold(report, credits) {
|
|
27
|
+
return report.rows.filter((r) => r.autumnCredits > credits).length;
|
|
18
28
|
}
|
|
19
29
|
export function usagePercentile(report, p) {
|
|
20
|
-
const usages = report.rows.map((r) => r.
|
|
30
|
+
const usages = report.rows.map((r) => r.autumnCredits).sort((a, b) => a - b);
|
|
21
31
|
if (usages.length === 0)
|
|
22
32
|
return 0;
|
|
23
33
|
const idx = Math.min(usages.length - 1, Math.max(0, Math.ceil((p / 100) * usages.length) - 1));
|
|
@@ -27,20 +37,25 @@ export function renderReconciliationMarkdown(report, thresholds) {
|
|
|
27
37
|
const lines = [];
|
|
28
38
|
lines.push("## Shadow-ledger reconciliation");
|
|
29
39
|
lines.push("");
|
|
30
|
-
lines.push(`Total DB cents: ${report.totalDbCents}`);
|
|
31
|
-
lines.push(`Total
|
|
32
|
-
lines.push(`Total
|
|
40
|
+
lines.push(`Total DB cents (raw): ${report.totalDbCents}`);
|
|
41
|
+
lines.push(`Total expected credits (2x): ${report.totalExpectedCredits}`);
|
|
42
|
+
lines.push(`Total Autumn credits: ${report.totalAutumnCredits}`);
|
|
43
|
+
lines.push(`Total drift credits: ${report.totalDriftCredits} (${report.driftPct.toFixed(2)}%)`);
|
|
44
|
+
lines.push("");
|
|
45
|
+
lines.push("Carve-outs (see header): wall-crossing turns under-count in Autumn by the");
|
|
46
|
+
lines.push("clamped overshoot (verify via events.list values); plan changes reset the");
|
|
47
|
+
lines.push("aggregate — exclude users who changed plan inside the window.");
|
|
33
48
|
lines.push("");
|
|
34
49
|
for (const t of thresholds) {
|
|
35
|
-
lines.push(`Users over ${t}
|
|
50
|
+
lines.push(`Users over ${t} credits: ${usersOverThreshold(report, t)}`);
|
|
36
51
|
}
|
|
37
52
|
lines.push("");
|
|
38
|
-
lines.push(`p50 / p90 / p99 usage
|
|
53
|
+
lines.push(`p50 / p90 / p99 usage credits: ${usagePercentile(report, 50)} / ${usagePercentile(report, 90)} / ${usagePercentile(report, 99)}`);
|
|
39
54
|
lines.push("");
|
|
40
|
-
lines.push("| userId | dbCents |
|
|
41
|
-
lines.push("| --- | --- | --- | --- |");
|
|
55
|
+
lines.push("| userId | dbCents | expectedCredits | autumnCredits | driftCredits |");
|
|
56
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
42
57
|
for (const r of report.rows) {
|
|
43
|
-
lines.push(`| ${r.userId} | ${r.dbCents} | ${r.
|
|
58
|
+
lines.push(`| ${r.userId} | ${r.dbCents} | ${r.expectedCredits} | ${r.autumnCredits} | ${r.driftCredits} |`);
|
|
44
59
|
}
|
|
45
60
|
return lines.join("\n");
|
|
46
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"billing-reconcile-util.js","sourceRoot":"","sources":["../../jsr/usage-report/billing-reconcile-util.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"billing-reconcile-util.js","sourceRoot":"","sources":["../../jsr/usage-report/billing-reconcile-util.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cross-plane usage monitor for a deployed vibe.
|
|
3
|
+
|
|
4
|
+
Pulls last-N-hours activity from all three data planes and prints one funnel:
|
|
5
|
+
- Postgres (AppDocuments): signed-in writers, doc types, new vs returning
|
|
6
|
+
- Cloudflare GraphQL: page visits / distinct IPs (read-only visitors too)
|
|
7
|
+
- Clerk Backend API: new sign-ups + platform baseline
|
|
8
|
+
|
|
9
|
+
This is the scripted form of the runbook: agents/vibe-usage-analytics.md
|
|
10
|
+
Read that for the gotchas (Clerk UA err-1010, CF 1d-window cap + sampling,
|
|
11
|
+
cumulative-vs-rolling-window). Read-only throughout.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python3 vibe-usage.py [HOURS] # default 48
|
|
15
|
+
OWNER=owner APP_SLUG=my-app CF_PATH='/vibe/owner/my-app%' python3 vibe-usage.py 168
|
|
16
|
+
|
|
17
|
+
Requires env (present in cloud sessions): CLOUDFLARE_API_TOKEN, CLERK_SECRET_KEY.
|
|
18
|
+
Postgres goes through the repo's db:inspect (prod, read-only).
|
|
19
|
+
"""
|
|
20
|
+
import json, os, subprocess, sys, urllib.request, urllib.error
|
|
21
|
+
from datetime import datetime, timedelta, timezone
|
|
22
|
+
|
|
23
|
+
HOURS = int(sys.argv[1]) if len(sys.argv) > 1 else 48
|
|
24
|
+
# App identity in Postgres is keyed by (userSlug, appSlug) — filtering on appSlug
|
|
25
|
+
# alone would aggregate dev/remix/test twins under other handles. OWNER scopes it
|
|
26
|
+
# to the one owner whose /vibe/<owner>/<slug> path the CF_PATH also targets.
|
|
27
|
+
OWNER = os.environ.get("OWNER", "og") # live app owner handle
|
|
28
|
+
APP_SLUG = os.environ.get("APP_SLUG", "pickathon-picker") # live app (owner: og)
|
|
29
|
+
CF_PATH = os.environ.get("CF_PATH", "/vibe/og/pickathon-picker%") # page shell path
|
|
30
|
+
CF_ZONE = os.environ.get("CF_ZONE", "13f147047a38c9d54a576424347cb4be") # vibes.diy
|
|
31
|
+
# Resolve DB_DIR relative to this file so it works from any cwd / worktree.
|
|
32
|
+
DB_DIR = os.path.dirname(os.path.abspath(__file__)) + "/.."
|
|
33
|
+
REPO_ROOT = os.path.abspath(DB_DIR + "/../../..")
|
|
34
|
+
|
|
35
|
+
now = datetime.now(timezone.utc)
|
|
36
|
+
since = now - timedelta(hours=HOURS)
|
|
37
|
+
since_iso = since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
38
|
+
now_iso = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
39
|
+
since_ms = int(since.timestamp() * 1000)
|
|
40
|
+
|
|
41
|
+
CF_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN")
|
|
42
|
+
CLERK_KEY = os.environ.get("CLERK_SECRET_KEY")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def db(sql):
|
|
46
|
+
"""Run a read-only query via db:inspect, return parsed rows."""
|
|
47
|
+
p = subprocess.run(
|
|
48
|
+
["pnpm", "--dir", DB_DIR, "run", "db:inspect", "sql", sql],
|
|
49
|
+
capture_output=True, text=True, cwd=REPO_ROOT,
|
|
50
|
+
)
|
|
51
|
+
if "{" not in p.stdout: # surface the real DB/tooling error, not a ValueError
|
|
52
|
+
raise SystemExit(f"db:inspect failed (exit {p.returncode}):\n"
|
|
53
|
+
f"{(p.stderr or p.stdout).strip()}")
|
|
54
|
+
return json.loads(p.stdout[p.stdout.index("{"):]).get("rows", [])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def http_json(url, data=None, headers=None):
|
|
58
|
+
headers = dict(headers or {})
|
|
59
|
+
# Clerk/CF firewall 403s (err 1010) the default python-urllib UA; curl UA passes.
|
|
60
|
+
headers.setdefault("User-Agent", "curl/8.0")
|
|
61
|
+
req = urllib.request.Request(url, data=data, headers=headers)
|
|
62
|
+
try:
|
|
63
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
64
|
+
return json.loads(r.read())
|
|
65
|
+
except urllib.error.HTTPError as e:
|
|
66
|
+
return {"_error": f"{e.code} {e.read()[:200]!r}"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cf_graphql(query):
|
|
70
|
+
return http_json(
|
|
71
|
+
"https://api.cloudflare.com/client/v4/graphql",
|
|
72
|
+
data=json.dumps({"query": query}).encode(),
|
|
73
|
+
headers={"Authorization": f"Bearer {CF_TOKEN}", "Content-Type": "application/json"},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def clerk(path):
|
|
78
|
+
return http_json("https://api.clerk.com/v1" + path,
|
|
79
|
+
headers={"Authorization": f"Bearer {CLERK_KEY}"})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
print(f"=== {APP_SLUG} — last {HOURS}h ({since_iso} → {now_iso}) ===\n")
|
|
83
|
+
|
|
84
|
+
# ---- 1. Postgres: signed-in activity -------------------------------------
|
|
85
|
+
APP_FILTER = f"""\"userSlug\" = '{OWNER}' AND "appSlug" = '{APP_SLUG}'"""
|
|
86
|
+
totals = db(f"""SELECT count(*) AS writes, count(DISTINCT "userId") AS writers,
|
|
87
|
+
count(DISTINCT "docId") AS docs, coalesce(sum(deleted),0) AS deletes
|
|
88
|
+
FROM "AppDocuments"
|
|
89
|
+
WHERE {APP_FILTER} AND created >= '{since_iso}'""")[0]
|
|
90
|
+
print(f"DATA (signed-in users) [{OWNER}/{APP_SLUG}]")
|
|
91
|
+
print(f" writes={totals['writes']} writers={totals['writers']} "
|
|
92
|
+
f"distinct-docs={totals['docs']} deletes={totals['deletes']}")
|
|
93
|
+
|
|
94
|
+
types = db(f"""SELECT coalesce(data->>'type','(none)') AS t, count(*) AS n,
|
|
95
|
+
count(DISTINCT "userId") AS u FROM "AppDocuments"
|
|
96
|
+
WHERE {APP_FILTER} AND created >= '{since_iso}'
|
|
97
|
+
GROUP BY 1 ORDER BY n DESC""")
|
|
98
|
+
print(" by type: " + ", ".join(f"{r['t']}={r['n']}({r['u']}u)" for r in types))
|
|
99
|
+
|
|
100
|
+
split = db(f"""WITH firsts AS (SELECT "userId", min(created) f FROM "AppDocuments"
|
|
101
|
+
WHERE {APP_FILTER} GROUP BY 1)
|
|
102
|
+
SELECT count(*) FILTER (WHERE f >= '{since_iso}') AS new,
|
|
103
|
+
count(*) FILTER (WHERE f < '{since_iso}') AS returning
|
|
104
|
+
FROM firsts WHERE "userId" IN (
|
|
105
|
+
SELECT DISTINCT "userId" FROM "AppDocuments"
|
|
106
|
+
WHERE {APP_FILTER} AND created >= '{since_iso}')""")[0]
|
|
107
|
+
print(f" writers new-to-app={split['new']} returning={split['returning']}")
|
|
108
|
+
|
|
109
|
+
# ---- 2. Cloudflare: read-only visitors (day-sliced; zone caps at 1d) ------
|
|
110
|
+
print("\nTRAFFIC (all visitors, incl. non-signed-in; sampled + IP counts overlap days)")
|
|
111
|
+
tot_ip = tot_visits = tot_req = 0
|
|
112
|
+
slice_start = since
|
|
113
|
+
while slice_start < now:
|
|
114
|
+
slice_end = min(slice_start + timedelta(hours=24), now)
|
|
115
|
+
q = f'''query {{ viewer {{ zones(filter: {{zoneTag: "{CF_ZONE}"}}) {{
|
|
116
|
+
httpRequestsAdaptiveGroups(limit: 5000, filter: {{
|
|
117
|
+
datetime_geq: "{slice_start.strftime('%Y-%m-%dT%H:%M:%SZ')}",
|
|
118
|
+
datetime_leq: "{slice_end.strftime('%Y-%m-%dT%H:%M:%SZ')}",
|
|
119
|
+
clientRequestPath_like: "{CF_PATH}"}}) {{
|
|
120
|
+
dimensions {{ clientIP }} sum {{ visits }} count }} }} }} }}'''
|
|
121
|
+
res = cf_graphql(q)
|
|
122
|
+
if res.get("errors") or res.get("_error"):
|
|
123
|
+
print(f" [{slice_start:%m-%d %H:%M}] cf error: {res.get('errors') or res.get('_error')}")
|
|
124
|
+
else:
|
|
125
|
+
rows = res["data"]["viewer"]["zones"][0]["httpRequestsAdaptiveGroups"]
|
|
126
|
+
ips = len(rows); v = sum(r["sum"]["visits"] for r in rows); c = sum(r["count"] for r in rows)
|
|
127
|
+
tot_ip += ips; tot_visits += v; tot_req += c
|
|
128
|
+
print(f" [{slice_start:%m-%d %H:%M}→{slice_end:%H:%M}] IPs={ips} visits={v} requests={c}")
|
|
129
|
+
slice_start = slice_end
|
|
130
|
+
print(f" TOTAL: IPs={tot_ip} (summed per-slice — cross-day overlap, not exact uniques)"
|
|
131
|
+
f" visits={tot_visits} requests={tot_req}")
|
|
132
|
+
|
|
133
|
+
# ---- 3. Clerk: sign-ups + baseline ---------------------------------------
|
|
134
|
+
print("\nSIGN-UPS (Clerk, platform-wide)")
|
|
135
|
+
total = clerk("/users/count").get("total_count", "?")
|
|
136
|
+
active = clerk(f"/users/count?last_active_at_since={since_ms}").get("total_count", "?")
|
|
137
|
+
# Page back until we pass the baseline window's start, else the newest 200 would
|
|
138
|
+
# silently cap `new`/`prior` during exactly the spike/long-window runs this targets.
|
|
139
|
+
base_start = since_ms - 14 * 86400000
|
|
140
|
+
users, offset, capped = [], 0, False
|
|
141
|
+
while True:
|
|
142
|
+
page = clerk(f"/users?order_by=-created_at&limit=500&offset={offset}")
|
|
143
|
+
if not isinstance(page, list) or not page:
|
|
144
|
+
break
|
|
145
|
+
users += page
|
|
146
|
+
if page[-1]["created_at"] < base_start: # covered the whole window+baseline
|
|
147
|
+
break
|
|
148
|
+
offset += 500
|
|
149
|
+
if offset >= 5000: # 10k-account safety stop
|
|
150
|
+
capped = True
|
|
151
|
+
break
|
|
152
|
+
new = [u for u in users if u["created_at"] >= since_ms]
|
|
153
|
+
prior = [u for u in users if base_start <= u["created_at"] < since_ms]
|
|
154
|
+
base_per = len(prior) / (14 / (HOURS / 24))
|
|
155
|
+
print(f" total accounts(cumulative)={total} active-in-window={active}")
|
|
156
|
+
cap_note = " ⚠ paging capped at 10k — new/baseline may undercount" if capped else ""
|
|
157
|
+
print(f" new sign-ups={len(new)} (prior baseline ≈{base_per:.1f} per {HOURS}h){cap_note}")
|
|
158
|
+
doms = {}
|
|
159
|
+
for u in new:
|
|
160
|
+
d = (u["email_addresses"][0]["email_address"].split("@")[-1]
|
|
161
|
+
if u.get("email_addresses") else "?")
|
|
162
|
+
doms[d] = doms.get(d, 0) + 1
|
|
163
|
+
if doms:
|
|
164
|
+
print(" domains: " + ", ".join(f"{k}={v}" for k, v in sorted(doms.items(), key=lambda x: -x[1])))
|
|
165
|
+
|
|
166
|
+
# funnel cross-ref: which new Clerk accounts wrote in the app
|
|
167
|
+
writers = {r["userId"] for r in db(f'''SELECT DISTINCT "userId" FROM "AppDocuments"
|
|
168
|
+
WHERE {APP_FILTER} AND created >= '{since_iso}' ''')}
|
|
169
|
+
new_ids = {u["id"] for u in new}
|
|
170
|
+
print(f"\nFUNNEL: ~{tot_visits} visits → {len(new)} sign-ups → "
|
|
171
|
+
f"{len(writers)} active writers ({len(writers & new_ids)} of them brand-new accounts)")
|