@viccydev/pi-fpa 0.3.1 → 0.3.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/extensions/fpa-data/index.ts +39 -5
- package/extensions/fpa-data/sql.ts +44 -11
- package/package.json +1 -1
|
@@ -23,16 +23,34 @@ import { CATALOG_CAVEATS, DATASETS, DATASET_IDS } from "./registry.ts";
|
|
|
23
23
|
import {
|
|
24
24
|
buildCohortQuery,
|
|
25
25
|
buildCoverageSql,
|
|
26
|
+
buildCoverageStatsSql,
|
|
26
27
|
buildCohortSizeCoverageSql,
|
|
28
|
+
COVERAGE_TARGETS,
|
|
27
29
|
MAX_LIMIT,
|
|
28
30
|
type QuerySpec,
|
|
29
31
|
} from "./sql.ts";
|
|
30
32
|
import { runStructuredQuery } from "./runtime.ts";
|
|
31
|
-
import { runQuery } from "./supabase.ts";
|
|
33
|
+
import { runQuery, type SqlRow } from "./supabase.ts";
|
|
32
34
|
|
|
33
35
|
const MAX_TOOL_TEXT_CHARS = 100_000;
|
|
34
36
|
const MAX_DISPLAY_ROWS = 200;
|
|
35
37
|
|
|
38
|
+
/** Full-scan catalog stats get a shorter leash than a real query; they are context, not answers. */
|
|
39
|
+
const CATALOG_STATS_TIMEOUT_MS = 12_000;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve to null when supplementary work fails, so one slow table degrades a field
|
|
43
|
+
* instead of the whole tool. A caller-initiated abort still propagates.
|
|
44
|
+
*/
|
|
45
|
+
async function bestEffort<T>(work: Promise<T>, signal?: AbortSignal): Promise<T | null> {
|
|
46
|
+
try {
|
|
47
|
+
return await work;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (signal?.aborted) throw error;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
const DatasetIdSchema = StringEnum(DATASET_IDS as [string, ...string[]], {
|
|
37
55
|
description: "Dataset id from fpa_data_catalog",
|
|
38
56
|
});
|
|
@@ -134,9 +152,19 @@ export default function fpaDataExtension(pi: ExtensionAPI): void {
|
|
|
134
152
|
),
|
|
135
153
|
executionMode: "parallel",
|
|
136
154
|
async execute(_toolCallId, params, signal) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
// Date coverage is the part callers must have, and min/max over an indexed date
|
|
156
|
+
// column is cheap. Run one query per dataset so a single slow table cannot time
|
|
157
|
+
// out the whole catalog, then gather the scan-bound stats separately.
|
|
158
|
+
const coverageRows = await Promise.all(
|
|
159
|
+
COVERAGE_TARGETS.map(async (target) => (await runQuery(buildCoverageSql(target), { signal }))[0] ?? { dataset: target.dataset }),
|
|
160
|
+
);
|
|
161
|
+
const [statsRows, cohortSize, apps] = await Promise.all([
|
|
162
|
+
Promise.all(
|
|
163
|
+
COVERAGE_TARGETS.map((target) =>
|
|
164
|
+
bestEffort(runQuery(buildCoverageStatsSql(target), { signal, timeoutMs: CATALOG_STATS_TIMEOUT_MS }), signal),
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
bestEffort(runQuery(buildCohortSizeCoverageSql(), { signal, timeoutMs: CATALOG_STATS_TIMEOUT_MS }), signal),
|
|
140
168
|
params.include_apps
|
|
141
169
|
? runQuery(
|
|
142
170
|
"select app_code, string_agg(distinct platform, ',' order by platform) as platforms " +
|
|
@@ -146,6 +174,12 @@ export default function fpaDataExtension(pi: ExtensionAPI): void {
|
|
|
146
174
|
)
|
|
147
175
|
: Promise.resolve<SqlRow[]>([]),
|
|
148
176
|
]);
|
|
177
|
+
const coverage = coverageRows.map((row, index) => {
|
|
178
|
+
const stats = statsRows[index]?.[0];
|
|
179
|
+
return stats
|
|
180
|
+
? { ...row, ...stats }
|
|
181
|
+
: { ...row, row_count: null, apps: null, updated_at: null, stats_unavailable: "Row counts timed out; date coverage above is unaffected." };
|
|
182
|
+
});
|
|
149
183
|
|
|
150
184
|
return toolResult(
|
|
151
185
|
{
|
|
@@ -165,7 +199,7 @@ export default function fpaDataExtension(pi: ExtensionAPI): void {
|
|
|
165
199
|
notes: d.notes,
|
|
166
200
|
})),
|
|
167
201
|
coverage,
|
|
168
|
-
cohort_size_available: cohortSize[0] ?? null,
|
|
202
|
+
cohort_size_available: cohortSize?.[0] ?? null,
|
|
169
203
|
caveats: CATALOG_CAVEATS,
|
|
170
204
|
...(params.include_apps ? { apps } : {}),
|
|
171
205
|
},
|
|
@@ -442,17 +442,50 @@ export function buildCohortQuery(spec: CohortSpec): BuiltCohortQuery {
|
|
|
442
442
|
return { sql, spendSql, asOfSql, horizons, bucket, groupBy };
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
/**
|
|
446
|
-
export
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
445
|
+
/** One row of the fpa_data_catalog coverage block. */
|
|
446
|
+
export interface CoverageTarget {
|
|
447
|
+
dataset: string;
|
|
448
|
+
table: string;
|
|
449
|
+
dateColumn: string;
|
|
450
|
+
appColumn: string;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export const COVERAGE_TARGETS: CoverageTarget[] = [
|
|
454
|
+
{ dataset: "ua_spend", table: "appsflyer_ua_campaign_daily", dateColumn: "install_date", appColumn: "app_code" },
|
|
455
|
+
{ dataset: "revenue_activity", table: "appsflyer_install_cohort_daily", dateColumn: "event_date", appColumn: "app_code" },
|
|
456
|
+
{ dataset: "cohort (fpa_cohort)", table: "appsflyer_install_cohort_daily", dateColumn: "install_date", appColumn: "app_code" },
|
|
457
|
+
{ dataset: "apple_store", table: "apple_app_daily", dateColumn: "metric_date", appColumn: "adam_id" },
|
|
458
|
+
{ dataset: "product_events", table: "appsflyer_event_daily", dateColumn: "event_date", appColumn: "app_code" },
|
|
459
|
+
{ dataset: "mixpanel_product", table: "mixpanel_app_daily", dateColumn: "business_date", appColumn: "app_code" },
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Answerable date range for one dataset.
|
|
464
|
+
*
|
|
465
|
+
* min()/max() alone let Postgres walk the date index instead of scanning the table.
|
|
466
|
+
* Mixing them with count(*) in a single statement — as the old six-way UNION did —
|
|
467
|
+
* forces a sequential scan per arm, which is what pushed fpa_data_catalog past its
|
|
468
|
+
* timeout as the mart grew.
|
|
469
|
+
*/
|
|
470
|
+
export function buildCoverageSql(target: CoverageTarget): string {
|
|
471
|
+
return assertReadOnly(
|
|
472
|
+
`select ${escapeLiteral(target.dataset)} as dataset, ` +
|
|
473
|
+
`min(${target.dateColumn})::text as date_min, max(${target.dateColumn})::text as date_max ` +
|
|
474
|
+
`from ${target.table}`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Row counts and freshness for one dataset. These need a full scan (count(distinct)
|
|
480
|
+
* most of all), so callers treat them as best-effort and report null when they time out
|
|
481
|
+
* rather than failing the whole catalog.
|
|
482
|
+
*/
|
|
483
|
+
export function buildCoverageStatsSql(target: CoverageTarget): string {
|
|
484
|
+
return assertReadOnly(
|
|
485
|
+
`select count(*) as row_count, count(distinct ${target.appColumn}) as apps, ` +
|
|
486
|
+
`max(updated_at)::text as updated_at ` +
|
|
487
|
+
`from ${target.table}`,
|
|
488
|
+
);
|
|
456
489
|
}
|
|
457
490
|
|
|
458
491
|
/** Cohort-size availability window, reported by fpa_data_catalog. */
|