@littlebearapps/platform-admin-sdk 1.4.2 → 1.5.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/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +57 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +3 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/package.json.hbs +12 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +2 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribution Check Module for Platform Mapper
|
|
3
|
+
*
|
|
4
|
+
* Checks whether discovered Cloudflare resources are properly attributed to projects.
|
|
5
|
+
* Runs every 15 minutes as part of platform-mapper's scheduled handler.
|
|
6
|
+
*
|
|
7
|
+
* @module workers/lib/mapper/attribution-check
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Logger } from '@littlebearapps/platform-consumer-sdk';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Environment bindings required for attribution checking.
|
|
14
|
+
* Uses generic types to work with both raw Env and TrackedEnv.
|
|
15
|
+
*/
|
|
16
|
+
export interface AttributionEnv {
|
|
17
|
+
PLATFORM_DB: {
|
|
18
|
+
prepare: (query: string) => {
|
|
19
|
+
bind: (...args: unknown[]) => {
|
|
20
|
+
run: () => Promise<unknown>;
|
|
21
|
+
first: <T>() => Promise<T | null>;
|
|
22
|
+
};
|
|
23
|
+
first: <T>() => Promise<T | null>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
PLATFORM_CACHE: {
|
|
27
|
+
get: (key: string) => Promise<string | null>;
|
|
28
|
+
put: (key: string, value: string, options?: { expirationTtl?: number }) => Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
SERVICE_REGISTRY: {
|
|
31
|
+
get: (key: string) => Promise<string | null>;
|
|
32
|
+
};
|
|
33
|
+
SLACK_WEBHOOK_URL?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Discovered resource
|
|
38
|
+
*/
|
|
39
|
+
export interface DiscoveredResource {
|
|
40
|
+
resourceType: string; // worker, d1, kv, r2, queue, do
|
|
41
|
+
resourceId: string;
|
|
42
|
+
resourceName: string;
|
|
43
|
+
metadata?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attribution result for a single resource
|
|
48
|
+
*/
|
|
49
|
+
export interface ResourceAttribution {
|
|
50
|
+
resource: DiscoveredResource;
|
|
51
|
+
project: string | null;
|
|
52
|
+
matchedPattern: string | null;
|
|
53
|
+
confidence: 'high' | 'medium' | 'low' | 'none';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Full attribution report
|
|
58
|
+
*/
|
|
59
|
+
export interface AttributionReport {
|
|
60
|
+
discoveryTime: string;
|
|
61
|
+
attributed: ResourceAttribution[];
|
|
62
|
+
unattributed: ResourceAttribution[];
|
|
63
|
+
totalResources: number;
|
|
64
|
+
attributedCount: number;
|
|
65
|
+
unattributedCount: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Project patterns for attribution.
|
|
70
|
+
* Maps resource name patterns to projects.
|
|
71
|
+
*
|
|
72
|
+
* TODO: Add your project patterns here. Each key is a project name,
|
|
73
|
+
* and the value is an array of RegExp patterns that match resource names
|
|
74
|
+
* belonging to that project.
|
|
75
|
+
*
|
|
76
|
+
* Example:
|
|
77
|
+
* 'my-app': [/^my-app-/i, /my-app-db/i, /my-app-kv/i],
|
|
78
|
+
* 'api': [/^api-/i, /api-worker/i],
|
|
79
|
+
*/
|
|
80
|
+
const PROJECT_PATTERNS: Record<string, RegExp[]> = {
|
|
81
|
+
// Add your project patterns here:
|
|
82
|
+
// 'project-name': [/^project-name-/i, /project-name-db/i],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resources to exclude from attribution (system/shared resources).
|
|
87
|
+
*
|
|
88
|
+
* TODO: Adjust these patterns to match resources you want to skip.
|
|
89
|
+
*/
|
|
90
|
+
const EXCLUDED_PATTERNS: RegExp[] = [
|
|
91
|
+
/^_/i, // Internal resources
|
|
92
|
+
/test/i, // Test resources
|
|
93
|
+
/dev-/i, // Dev resources
|
|
94
|
+
/staging/i, // Staging resources
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check attribution of discovered resources
|
|
99
|
+
*/
|
|
100
|
+
export function checkAttribution(resources: DiscoveredResource[]): AttributionReport {
|
|
101
|
+
const report: AttributionReport = {
|
|
102
|
+
discoveryTime: new Date().toISOString(),
|
|
103
|
+
attributed: [],
|
|
104
|
+
unattributed: [],
|
|
105
|
+
totalResources: 0,
|
|
106
|
+
attributedCount: 0,
|
|
107
|
+
unattributedCount: 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (const resource of resources) {
|
|
111
|
+
// Skip excluded resources
|
|
112
|
+
if (EXCLUDED_PATTERNS.some((pattern) => pattern.test(resource.resourceName))) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
report.totalResources++;
|
|
117
|
+
const attribution = attributeResource(resource);
|
|
118
|
+
|
|
119
|
+
if (attribution.project) {
|
|
120
|
+
report.attributed.push(attribution);
|
|
121
|
+
report.attributedCount++;
|
|
122
|
+
} else {
|
|
123
|
+
report.unattributed.push(attribution);
|
|
124
|
+
report.unattributedCount++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return report;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Attribute a single resource to a project
|
|
133
|
+
*/
|
|
134
|
+
function attributeResource(resource: DiscoveredResource): ResourceAttribution {
|
|
135
|
+
for (const [project, patterns] of Object.entries(PROJECT_PATTERNS)) {
|
|
136
|
+
for (const pattern of patterns) {
|
|
137
|
+
if (pattern.test(resource.resourceName)) {
|
|
138
|
+
return {
|
|
139
|
+
resource,
|
|
140
|
+
project,
|
|
141
|
+
matchedPattern: pattern.source,
|
|
142
|
+
confidence: 'high',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Try to infer from resource name structure
|
|
149
|
+
const suggestedProject = suggestProject(resource.resourceName);
|
|
150
|
+
if (suggestedProject) {
|
|
151
|
+
return {
|
|
152
|
+
resource,
|
|
153
|
+
project: suggestedProject,
|
|
154
|
+
matchedPattern: 'name_inference',
|
|
155
|
+
confidence: 'low',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
resource,
|
|
161
|
+
project: null,
|
|
162
|
+
matchedPattern: null,
|
|
163
|
+
confidence: 'none',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Suggest a project from resource name using heuristics.
|
|
169
|
+
*
|
|
170
|
+
* TODO: Update this function to match your project naming conventions.
|
|
171
|
+
* The heuristics below look for common substrings in resource names.
|
|
172
|
+
*/
|
|
173
|
+
function suggestProject(resourceName: string): string | null {
|
|
174
|
+
const lowerName = resourceName.toLowerCase();
|
|
175
|
+
|
|
176
|
+
// Check against PROJECT_PATTERNS keys as substring match
|
|
177
|
+
for (const project of Object.keys(PROJECT_PATTERNS)) {
|
|
178
|
+
if (lowerName.includes(project.toLowerCase())) {
|
|
179
|
+
return project;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Store attribution report in D1
|
|
188
|
+
*/
|
|
189
|
+
export async function storeAttributionReport(
|
|
190
|
+
env: AttributionEnv,
|
|
191
|
+
report: AttributionReport,
|
|
192
|
+
log: Logger
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
const id = crypto.randomUUID();
|
|
196
|
+
|
|
197
|
+
await env.PLATFORM_DB.prepare(
|
|
198
|
+
`
|
|
199
|
+
INSERT INTO attribution_reports (id, discovery_time, total_resources, attributed_count, unattributed_count, report_json)
|
|
200
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
201
|
+
`
|
|
202
|
+
)
|
|
203
|
+
.bind(
|
|
204
|
+
id,
|
|
205
|
+
report.discoveryTime,
|
|
206
|
+
report.totalResources,
|
|
207
|
+
report.attributedCount,
|
|
208
|
+
report.unattributedCount,
|
|
209
|
+
JSON.stringify(report)
|
|
210
|
+
)
|
|
211
|
+
.run();
|
|
212
|
+
|
|
213
|
+
log.debug('Stored attribution report', {
|
|
214
|
+
id,
|
|
215
|
+
total: report.totalResources,
|
|
216
|
+
unattributed: report.unattributedCount,
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
log.error('Failed to store attribution report', error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Alert on unattributed resources via Slack
|
|
225
|
+
*/
|
|
226
|
+
export async function alertUnattributedResources(
|
|
227
|
+
env: AttributionEnv,
|
|
228
|
+
report: AttributionReport,
|
|
229
|
+
log: Logger
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
// Only alert if there are unattributed resources
|
|
232
|
+
if (report.unattributedCount === 0) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check rate limit (1 alert per 4 hours)
|
|
237
|
+
const alertKey = 'attribution:alert';
|
|
238
|
+
const alreadySent = await env.PLATFORM_CACHE.get(alertKey);
|
|
239
|
+
|
|
240
|
+
if (alreadySent) {
|
|
241
|
+
log.debug('Attribution alert rate limited');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!env.SLACK_WEBHOOK_URL) {
|
|
246
|
+
log.debug('No SLACK_WEBHOOK_URL configured, skipping attribution alert');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Build summary of unattributed resources
|
|
251
|
+
const summary = report.unattributed
|
|
252
|
+
.slice(0, 10)
|
|
253
|
+
.map((a) => `${a.resource.resourceType}: ${a.resource.resourceName}`)
|
|
254
|
+
.join('\n');
|
|
255
|
+
|
|
256
|
+
const message = {
|
|
257
|
+
text: `[WARNING] ${report.unattributedCount} Cloudflare resources not attributed to projects`,
|
|
258
|
+
blocks: [
|
|
259
|
+
{
|
|
260
|
+
type: 'header',
|
|
261
|
+
text: {
|
|
262
|
+
type: 'plain_text',
|
|
263
|
+
text: 'Unattributed Resources Detected',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
type: 'section',
|
|
268
|
+
fields: [
|
|
269
|
+
{ type: 'mrkdwn', text: `*Total Resources:*\n${report.totalResources}` },
|
|
270
|
+
{ type: 'mrkdwn', text: `*Attributed:*\n${report.attributedCount}` },
|
|
271
|
+
{ type: 'mrkdwn', text: `*Unattributed:*\n${report.unattributedCount}` },
|
|
272
|
+
{ type: 'mrkdwn', text: `*Discovery Time:*\n${report.discoveryTime}` },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
type: 'section',
|
|
277
|
+
text: {
|
|
278
|
+
type: 'mrkdwn',
|
|
279
|
+
text: `*Unattributed Resources (first 10):*\n\`\`\`${summary}\`\`\``,
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
type: 'section',
|
|
284
|
+
text: {
|
|
285
|
+
type: 'mrkdwn',
|
|
286
|
+
text: `*Action Required:*\nUpdate PROJECT_PATTERNS in \`workers/lib/mapper/attribution-check.ts\` to include patterns for these resources.`,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const response = await fetch(env.SLACK_WEBHOOK_URL, {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
296
|
+
body: JSON.stringify(message),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (response.ok) {
|
|
300
|
+
// Set rate limit (4 hours)
|
|
301
|
+
await env.PLATFORM_CACHE.put(alertKey, new Date().toISOString(), {
|
|
302
|
+
expirationTtl: 14400,
|
|
303
|
+
});
|
|
304
|
+
log.info('Sent attribution alert', { unattributed: report.unattributedCount });
|
|
305
|
+
} else {
|
|
306
|
+
const text = await response.text();
|
|
307
|
+
log.error('Failed to send attribution alert', undefined, { status: response.status, error: text });
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
log.error('Error sending attribution alert', error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get latest attribution report from D1
|
|
316
|
+
*/
|
|
317
|
+
export async function getLatestAttributionReport(
|
|
318
|
+
env: AttributionEnv,
|
|
319
|
+
log: Logger
|
|
320
|
+
): Promise<AttributionReport | null> {
|
|
321
|
+
try {
|
|
322
|
+
const result = await env.PLATFORM_DB.prepare(
|
|
323
|
+
`
|
|
324
|
+
SELECT report_json
|
|
325
|
+
FROM attribution_reports
|
|
326
|
+
ORDER BY discovery_time DESC
|
|
327
|
+
LIMIT 1
|
|
328
|
+
`
|
|
329
|
+
).first<{ report_json: string }>();
|
|
330
|
+
|
|
331
|
+
if (result?.report_json) {
|
|
332
|
+
return JSON.parse(result.report_json) as AttributionReport;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
log.error('Failed to get latest attribution report', error);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|