/** * Seeds a complete Insights development environment from scratch. * * Creates: owner account, org, project, target, schema, usage data (30 days), * and saved filters with view counts. * * Prerequisites: * - Docker Compose is running (pnpm local:setup) * - Services are running (pnpm dev:hive) * * Usage: * bun scripts/seed-insights.mts */ import * as readline from 'node:readline/promises'; import setCookie from 'set-cookie-parser'; import type { CollectedOperation } from '../integration-tests/testkit/usage'; process.env.RUN_AGAINST_LOCAL_SERVICES = '1'; await import('../integration-tests/local-dev.ts'); const { ensureEnv } = await import('../integration-tests/testkit/env'); const { createOrganization, createProject, createToken, publishSchema } = await import( '../integration-tests/testkit/flow' ); const { execute } = await import('../integration-tests/testkit/graphql'); const { legacyCollect } = await import('../integration-tests/testkit/usage'); const { generateUnique, getServiceHost } = await import('../integration-tests/testkit/utils'); const { TargetAccessScope, ProjectType, SavedFilterVisibilityType } = await import( '../integration-tests/testkit/gql/graphql' ); const { CreateSavedFilterMutation, TrackSavedFilterViewMutation } = await import( '../integration-tests/testkit/saved-filters' ); // --------------------------------------------------------------------------- // Auth helper — handles both new and existing SuperTokens users // --------------------------------------------------------------------------- const password = 'ilikebigturtlesandicannotlie47'; async function signInOrSignUp( email: string, ): Promise<{ access_token: string; refresh_token: string }> { const graphqlAddress = await getServiceHost('server', 8082); let response = await fetch(`http://${graphqlAddress}/auth-api/signup`, { method: 'POST', body: JSON.stringify({ formFields: [ { id: 'email', value: email, }, { id: 'password', value: password, }, ], }), headers: { 'content-type': 'application/json', }, }); let body = await response.json(); if (body.status === 'OK') { const cookies = setCookie.parse(response.headers.getSetCookie()); return { access_token: cookies.find(c => c.name === 'sAccessToken')?.value ?? '', refresh_token: cookies.find(c => c.name === 'sRefreshToken')?.value ?? '', }; } console.log('signup response', JSON.stringify(body, null, 2)); console.log('attempt sign in'); response = await fetch(`http://${graphqlAddress}/auth-api/signin`, { method: 'POST', body: JSON.stringify({ formFields: [ { id: 'email', value: email, }, { id: 'password', value: password, }, ], }), headers: { 'content-type': 'application/json', }, }); body = await response.json(); if (body.status === 'OK') { const cookies = setCookie.parse(response.headers.getSetCookie()); return { access_token: cookies.find(c => c.name === 'sAccessToken')?.value ?? '', refresh_token: cookies.find(c => c.name === 'sRefreshToken')?.value ?? '', }; } throw new Error('Failed to sign in or up ' + JSON.stringify(body, null, 2)); } // --------------------------------------------------------------------------- // 1. Operations — ~1000 distinct queries/mutations against the Star Wars schema // --------------------------------------------------------------------------- type OperationDef = { operation: string; operationName: string; fields: string[]; }; const EPISODES = ['NEWHOPE', 'EMPIRE', 'JEDI'] as const; // Field selection templates for Character const CHARACTER_SELECTIONS = [ { body: 'name', fields: ['Character', 'Character.name'] }, { body: 'name appearsIn', fields: ['Character', 'Character.name', 'Character.appearsIn'] }, { body: 'name friends { name }', fields: ['Character', 'Character.name', 'Character.friends'] }, { body: 'name appearsIn friends { name }', fields: ['Character', 'Character.name', 'Character.appearsIn', 'Character.friends'], }, { body: 'name friends { name appearsIn }', fields: ['Character', 'Character.name', 'Character.friends', 'Character.appearsIn'], }, { body: 'name friends { name friends { name } }', fields: ['Character', 'Character.name', 'Character.friends'], }, ]; // Inline fragment templates const HUMAN_SELECTIONS = [ { body: '... on Human { name starships { name } }', fields: ['Human', 'Human.name', 'Human.starships', 'Starship', 'Starship.name'], }, { body: '... on Human { name totalCredits }', fields: ['Human', 'Human.name', 'Human.totalCredits'], }, { body: '... on Human { name starships { name length } }', fields: [ 'Human', 'Human.name', 'Human.starships', 'Starship', 'Starship.name', 'Starship.length', ], }, { body: '... on Human { name totalCredits starships { name } }', fields: [ 'Human', 'Human.name', 'Human.totalCredits', 'Human.starships', 'Starship', 'Starship.name', ], }, ]; const DROID_SELECTIONS = [ { body: '... on Droid { name primaryFunction }', fields: ['Droid', 'Droid.name', 'Droid.primaryFunction'], }, { body: '... on Droid { name }', fields: ['Droid', 'Droid.name'], }, ]; const REVIEW_SELECTIONS = [ { body: 'stars', fields: ['Review', 'Review.stars'] }, { body: 'stars commentary', fields: ['Review', 'Review.stars', 'Review.commentary'] }, { body: 'episode stars', fields: ['Review', 'Review.episode', 'Review.stars'] }, { body: 'episode stars commentary', fields: ['Review', 'Review.episode', 'Review.stars', 'Review.commentary'], }, ]; function generateOperations(): OperationDef[] { const ops: OperationDef[] = []; let idx = 0; // 1. Simple hero queries per episode x character selection (3 × 6 = 18) for (const ep of EPISODES) { for (const sel of CHARACTER_SELECTIONS) { const name = `GetHero_${idx++}`; ops.push({ operation: `query ${name} { hero(episode: ${ep}) { ${sel.body} } }`, operationName: name, fields: ['Query', 'Query.hero', ...sel.fields], }); } } // 2. Human fragment queries per episode (3 × 4 = 12) for (const ep of EPISODES) { for (const sel of HUMAN_SELECTIONS) { const name = `GetHuman_${idx++}`; ops.push({ operation: `query ${name} { hero(episode: ${ep}) { ${sel.body} } }`, operationName: name, fields: ['Query', 'Query.hero', ...sel.fields], }); } } // 3. Droid fragment queries per episode (3 × 2 = 6) for (const ep of EPISODES) { for (const sel of DROID_SELECTIONS) { const name = `GetDroid_${idx++}`; ops.push({ operation: `query ${name} { hero(episode: ${ep}) { ${sel.body} } }`, operationName: name, fields: ['Query', 'Query.hero', ...sel.fields], }); } } // 4. Combined Human + Droid queries per episode (3 × 4 × 2 = 24) for (const ep of EPISODES) { for (const hSel of HUMAN_SELECTIONS) { for (const dSel of DROID_SELECTIONS) { const name = `GetCharacterDetails_${idx++}`; ops.push({ operation: `query ${name} { hero(episode: ${ep}) { name ${hSel.body} ${dSel.body} } }`, operationName: name, fields: [ 'Query', 'Query.hero', 'Character', 'Character.name', ...hSel.fields, ...dSel.fields, ], }); } } } // 5. Multi-alias queries — different episode combos (3 choose 2 = 3, with varying selections = ~18) const epPairs: [string, string][] = [ ['NEWHOPE', 'EMPIRE'], ['NEWHOPE', 'JEDI'], ['EMPIRE', 'JEDI'], ]; for (const [ep1, ep2] of epPairs) { for (const sel of CHARACTER_SELECTIONS) { const name = `Compare_${idx++}`; ops.push({ operation: `query ${name} { a: hero(episode: ${ep1}) { ${sel.body} } b: hero(episode: ${ep2}) { ${sel.body} } }`, operationName: name, fields: ['Query', 'Query.hero', ...sel.fields], }); } } // 6. Triple-alias queries (1 × 6 = 6) for (const sel of CHARACTER_SELECTIONS) { const name = `AllEpisodeHeroes_${idx++}`; ops.push({ operation: `query ${name} { newhope: hero(episode: NEWHOPE) { ${sel.body} } empire: hero(episode: EMPIRE) { ${sel.body} } jedi: hero(episode: JEDI) { ${sel.body} } }`, operationName: name, fields: ['Query', 'Query.hero', ...sel.fields], }); } // 7. Mutation variations — createReview per episode x review selection (3 × 4 = 12) const STARS = [1, 2, 3, 4, 5]; for (const ep of EPISODES) { for (const sel of REVIEW_SELECTIONS) { const name = `CreateReview_${idx++}`; const stars = STARS[idx % STARS.length]; ops.push({ operation: `mutation ${name} { createReview(episode: ${ep}, review: { stars: ${stars} }) { ${sel.body} } }`, operationName: name, fields: ['Mutation', 'Mutation.createReview', ...sel.fields], }); } } // 8. Generate more unique queries to reach ~1000 by varying naming patterns // Simulate realistic operation names like a real codebase would have // Mix of short and long prefixes to test truncation at varying widths const PREFIXES = [ 'Dashboard', 'Settings', 'Profile', 'Admin', 'Search', 'Feed', 'Sync', 'OrganizationBillingSubscriptionDetails', 'TargetSchemaVersionComparison', 'ProjectAccessTokenPermissionsManagement', 'UserNotificationPreferencesUpdate', 'SchemaRegistryExplorerTypeDetails', 'IntegrationWebhookDeliveryStatus', 'AlertChannelConfigurationValidation', 'PersistedOperationCollectionSync', 'GraphQLEndpointLatencyPercentiles', 'CDNAccessTokenRotation', 'SchemaContractCompositionValidation', 'MemberRoleAssignmentAuditLog', 'OperationBodyNormalizationPreview', ]; const SUFFIXES = [ 'Query', 'Fetch', 'Load', 'Get', 'List', 'Detail', 'Summary', 'Overview', 'Stats', 'Count', 'WithPaginationAndFilters', 'ByOrganizationSlug', 'ForDateRangeComparison', ]; while (ops.length < 1000) { const prefix = PREFIXES[idx % PREFIXES.length]; const suffix = SUFFIXES[Math.floor(idx / PREFIXES.length) % SUFFIXES.length]; const ep = EPISODES[idx % 3]; const charSel = CHARACTER_SELECTIONS[idx % CHARACTER_SELECTIONS.length]; const name = `${prefix}${suffix}_${idx++}`; ops.push({ operation: `query ${name} { hero(episode: ${ep}) { ${charSel.body} } }`, operationName: name, fields: ['Query', 'Query.hero', ...charSel.fields], }); } return ops; } const OPERATIONS = generateOperations(); // --------------------------------------------------------------------------- // 2. Clients — 7 clients with 0–30 versions each // --------------------------------------------------------------------------- interface ClientDef { name: string; versions: string[]; weight: number; // relative traffic weight } function generateVersions(prefix: string, count: number): string[] { return Array.from({ length: count }, (_, i) => `${prefix}.${i}.0`); } // Mix of short and long client names to test truncation at varying widths const CLIENTS: ClientDef[] = [ { name: 'web-app', versions: generateVersions('1', 15), weight: 30 }, { name: 'ios', versions: generateVersions('2', 25), weight: 25 }, { name: 'android', versions: generateVersions('3', 10), weight: 15 }, { name: 'graphql-playground', versions: [], weight: 5 }, { name: 'admin-dashboard-internal-tools', versions: generateVersions('1', 5), weight: 8 }, { name: 'mobile-backend-for-frontend-service', versions: generateVersions('0', 30), weight: 12, }, { name: 'analytics-pipeline-worker-v2', versions: generateVersions('1', 8), weight: 5 }, ]; const TOTAL_WEIGHT = CLIENTS.reduce((s, c) => s + c.weight, 0); function pickWeightedClient(): { name: string; version: string | undefined } { let r = Math.random() * TOTAL_WEIGHT; for (const client of CLIENTS) { r -= client.weight; if (r <= 0) { const version = client.versions.length > 0 ? client.versions[Math.floor(Math.random() * client.versions.length)] : undefined; return { name: client.name, version }; } } // fallback return { name: CLIENTS[0].name, version: CLIENTS[0].versions[0] }; } function randomDuration(): number { // 50ms to 2s in nanoseconds, with most clustering around 100-500ms const base = 50 + Math.random() * 450; // 50-500ms const spike = Math.random() < 0.1 ? Math.random() * 1500 : 0; // 10% chance of slow return Math.round((base + spike) * 1_000_000); // convert ms → ns } function randomOperation() { return OPERATIONS[Math.floor(Math.random() * OPERATIONS.length)]; } // --------------------------------------------------------------------------- // 3. Schema SDL (Star Wars mono — matches scripts/seed-schemas/mono.graphql) // --------------------------------------------------------------------------- const SCHEMA_SDL = ` interface Node { id: ID! } interface Character implements Node { id: ID! name: String! friends: [Character] appearsIn: [Episode]! } type Human implements Character & Node { id: ID! name: String! friends: [Character] appearsIn: [Episode]! starships: [Starship] totalCredits: Int } type Droid implements Character & Node { id: ID! name: String! friends: [Character] appearsIn: [Episode]! primaryFunction: String } type Starship { id: ID! name: String! length(unit: LengthUnit = METER): Float } enum LengthUnit { METER LIGHT_YEAR } enum Episode { NEWHOPE EMPIRE JEDI } type Query { hero(episode: Episode): Character } type Review { episode: Episode stars: Int! commentary: String } input ReviewInput { stars: Int! commentary: String } type Mutation { createReview(episode: Episode, review: ReviewInput!): Review } `.trim(); // --------------------------------------------------------------------------- // 4. Prompt + Main // --------------------------------------------------------------------------- const BATCH_SIZE = 500; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; const ONE_HOUR_MS = 60 * 60 * 1000; async function promptForEmail(): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { const email = await rl.question('Enter owner email (or press Enter to auto-generate): '); return email.trim(); } finally { rl.close(); } } async function main() { const inputEmail = await promptForEmail(); const ownerEmail = inputEmail || `${generateUnique()}-${Date.now()}@localhost.localhost`; console.log(`\n🚀 Creating owner (${ownerEmail}), org, project...`); const auth = await signInOrSignUp(ownerEmail); const ownerToken = auth.access_token; // Create organization const orgSlug = generateUnique(); const orgResult = await createOrganization({ slug: orgSlug }, ownerToken).then(r => r.expectNoGraphQLErrors(), ); const organization = orgResult.createOrganization.ok!.createdOrganizationPayload.organization; // Create project const projectResult = await createProject( { organization: { bySelector: { organizationSlug: organization.slug } }, type: ProjectType.Single, slug: generateUnique(), }, ownerToken, ).then(r => r.expectNoGraphQLErrors()); const project = projectResult.createProject.ok!.createdProject; const target = projectResult.createProject.ok!.createdTargets[0]; console.log(` Org: ${organization.slug}`); console.log(` Project: ${project.slug}`); console.log(` Target: ${target.slug}`); // Create access token console.log('📝 Publishing schema...'); const tokenResult = await createToken( { name: generateUnique(), organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, ownerToken, ).then(r => r.expectNoGraphQLErrors()); const secret = tokenResult.createToken.ok!.secret; const publishResult = await publishSchema( { author: 'seed-insights', commit: 'seed', sdl: SCHEMA_SDL, force: true, }, secret, 'authorization', ); if (publishResult.rawBody.errors?.length) { console.error('Schema publish failed:', publishResult.rawBody.errors); process.exit(1); } console.log(' Schema published successfully.'); // Generate operations spread across 30 days console.log('📊 Generating usage data (30 days)...'); const now = Date.now(); const allOperations: CollectedOperation[] = []; for (let t = now - THIRTY_DAYS_MS; t <= now; t += ONE_HOUR_MS) { const opsThisHour = 3 + Math.floor(Math.random() * 6); // 3–8 per hour for (let i = 0; i < opsThisHour; i++) { const op = randomOperation(); const client = pickWeightedClient(); const ok = Math.random() > 0.05; allOperations.push({ timestamp: t + Math.floor(Math.random() * ONE_HOUR_MS), operation: op.operation, operationName: op.operationName, fields: op.fields, execution: { ok, duration: randomDuration(), errorsTotal: ok ? 0 : 1, }, metadata: { client: { name: client.name, ...(client.version ? { version: client.version } : {}), }, }, }); } } console.log(` Generated ${allOperations.length} operations across 30 days.`); // Send in batches const totalBatches = Math.ceil(allOperations.length / BATCH_SIZE); for (let i = 0; i < allOperations.length; i += BATCH_SIZE) { const batch = allOperations.slice(i, i + BATCH_SIZE); const batchNum = Math.floor(i / BATCH_SIZE) + 1; process.stdout.write(` Sending batch ${batchNum}/${totalBatches}...`); const result = await legacyCollect({ operations: batch, token: secret, authorizationHeader: 'authorization', }); if (result.status !== 200) { console.error(` FAILED (status ${result.status}):`, result.body); } else { const body = result.body as { operations: { accepted: number; rejected: number } }; console.log(` ✓ ${body.operations.accepted} accepted, ${body.operations.rejected} rejected`); } } // Wait for ingestion console.log('⏳ Waiting for usage ingestion (15s)...'); await new Promise(resolve => setTimeout(resolve, 15_000)); // Helper for saved filter operations const targetSelector = { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }; // Fetch actual operation hashes from ingested data console.log('🔍 Fetching operation hashes...'); const { parse } = await import('graphql'); const opsResult = await execute({ document: parse(/* GraphQL */ ` query SeedGetOperationHashes($target: TargetReferenceInput!, $period: DateRangeInput!) { target(reference: $target) { operationsStats(period: $period) { operations { edges { node { name operationHash } } } } } } `) as any, variables: { target: { bySelector: targetSelector }, period: { from: new Date(now - THIRTY_DAYS_MS).toISOString(), to: new Date(now).toISOString(), }, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); type OpsResult = { target?: { operationsStats?: { operations?: { edges: Array<{ node: { name: string; operationHash: string | null } }>; }; }; }; }; const operationEdges = (opsResult as OpsResult).target?.operationsStats?.operations?.edges ?? []; const operationHashMap = new Map(); for (const edge of operationEdges) { if (edge.node.operationHash && edge.node.name) { // API returns names as "{hashPrefix}_{operationName}", extract the operationName part const underscoreIdx = edge.node.name.indexOf('_'); const operationName = underscoreIdx >= 0 ? edge.node.name.slice(underscoreIdx + 1) : edge.node.name; operationHashMap.set(operationName, edge.node.operationHash); } } console.log( ` Found ${operationHashMap.size} operations: ${[...operationHashMap.keys()].join(', ')}`, ); async function createSavedFilter(input: { name: string; description?: string; visibility: (typeof SavedFilterVisibilityType)[keyof typeof SavedFilterVisibilityType]; insightsFilter?: Record; }) { const result = await execute({ document: CreateSavedFilterMutation, variables: { input: { target: { bySelector: targetSelector }, name: input.name, description: input.description, visibility: input.visibility, insightsFilter: input.insightsFilter, }, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); return result.createSavedFilter; } async function trackSavedFilterView(filterId: string) { await execute({ document: TrackSavedFilterViewMutation, variables: { input: { target: { bySelector: targetSelector }, id: filterId, }, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); } // Create saved filters using real operation hashes console.log('💾 Creating saved filters...'); // Helper to resolve operation names to hashes function resolveOps(names: string[]): string[] { return names.map(name => operationHashMap.get(name)).filter((h): h is string => h != null); } // Collect all known operation names for building saved filters const allOpNames = [...operationHashMap.keys()]; console.log(` Using ${allOpNames.length} resolved operations for saved filters`); // Define all saved filters (using slices of the resolved operation names) const savedFilterDefs: Array<{ name: string; description?: string; visibility: (typeof SavedFilterVisibilityType)[keyof typeof SavedFilterVisibilityType]; operationNames: string[]; clientFilters?: Array<{ name: string; versions?: string[] }>; dateRange?: { from: string; to: string }; views: number; }> = [ { name: 'High Traffic Operations', description: 'Most frequently called queries', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.slice(0, 20), clientFilters: [{ name: 'web-app' }, { name: 'ios' }], dateRange: { from: 'now-7d', to: 'now' }, views: 372, }, { name: 'Mobile Clients', description: 'iOS and Android traffic', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.slice(10, 25), clientFilters: [ { name: 'ios', versions: ['2.0.0', '2.5.0', '2.10.0', '2.20.0'] }, { name: 'android', versions: ['3.0.0', '3.5.0', '3.9.0'] }, ], dateRange: { from: 'now-30d', to: 'now' }, views: 156, }, { name: 'My Debug View', description: 'Debugging slow mutations', visibility: SavedFilterVisibilityType.Private, operationNames: allOpNames.slice(30, 35), clientFilters: [{ name: 'graphql-playground' }], dateRange: { from: 'now-1d', to: 'now' }, views: 23, }, { name: 'Web App — All Queries', description: 'All query operations from the web app', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.slice(0, 50), clientFilters: [{ name: 'web-app' }], dateRange: { from: 'now-7d', to: 'now' }, views: 241, }, { name: 'Mutations Only', description: 'All mutation operations across all clients', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.filter(n => n.startsWith('CreateReview')), dateRange: { from: 'now-14d', to: 'now' }, views: 89, }, { name: 'Admin Dashboard', description: 'Operations from the admin dashboard client', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.filter(n => n.startsWith('Dashboard')), clientFilters: [{ name: 'admin-dashboard-internal-tools' }], dateRange: { from: 'now-7d', to: 'now' }, views: 64, }, { name: 'BFF Layer', description: 'Mobile BFF service traffic', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.slice(50, 80), clientFilters: [{ name: 'mobile-backend-for-frontend-service' }], dateRange: { from: 'now-30d', to: 'now' }, views: 118, }, { name: 'Error Investigation', description: 'Operations with known error patterns', visibility: SavedFilterVisibilityType.Private, operationNames: allOpNames.slice(80, 90), clientFilters: [{ name: 'android', versions: ['3.0.0'] }], dateRange: { from: 'now-7d', to: 'now' }, views: 7, }, { name: 'Character Detail Queries', description: 'All character detail operations', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.filter(n => n.startsWith('GetCharacterDetails')), dateRange: { from: 'now-90d', to: 'now' }, views: 195, }, { name: 'Deep Queries', description: 'Queries with nested friend relationships', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.slice(3, 8), dateRange: { from: 'now-14d', to: 'now' }, views: 43, }, { name: 'iOS Latest Versions', description: 'Recent iOS app versions only', visibility: SavedFilterVisibilityType.Private, operationNames: allOpNames.slice(0, 15), clientFilters: [{ name: 'ios', versions: ['2.20.0', '2.24.0'] }], dateRange: { from: 'now-7d', to: 'now' }, views: 31, }, { name: 'Analytics Worker', description: 'Background analytics service operations', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.filter(n => n.startsWith('Analytics')), clientFilters: [{ name: 'analytics-pipeline-worker-v2' }], dateRange: { from: 'now-30d', to: 'now' }, views: 12, }, { name: 'Playground Exploration', description: 'Ad-hoc queries from GraphQL Playground', visibility: SavedFilterVisibilityType.Private, operationNames: allOpNames.slice(100, 120), clientFilters: [{ name: 'graphql-playground' }], dateRange: { from: 'now-1d', to: 'now' }, views: 5, }, { name: 'Droid & Human Types', description: 'Operations touching Droid and Human types', visibility: SavedFilterVisibilityType.Shared, operationNames: [ ...allOpNames.filter(n => n.startsWith('GetDroid')), ...allOpNames.filter(n => n.startsWith('GetHuman')), ], dateRange: { from: 'now-14d', to: 'now' }, views: 77, }, { name: 'Production Canary', description: 'Canary release monitoring — latest client versions', visibility: SavedFilterVisibilityType.Shared, operationNames: allOpNames.slice(0, 10), clientFilters: [ { name: 'web-app', versions: ['1.14.0'] }, { name: 'ios', versions: ['2.24.0'] }, { name: 'android', versions: ['3.9.0'] }, ], dateRange: { from: 'now-7d', to: 'now' }, views: 203, }, ]; // Create all filters and collect results const createdFilters: Array<{ name: string; id: string; views: number }> = []; for (const def of savedFilterDefs) { const ops = resolveOps(def.operationNames); const result = await createSavedFilter({ name: def.name, description: def.description, visibility: def.visibility, insightsFilter: { operationHashes: ops, ...(def.clientFilters ? { clientFilters: def.clientFilters } : {}), ...(def.dateRange ? { dateRange: def.dateRange } : {}), }, }); if (result.ok) { createdFilters.push({ name: def.name, id: result.ok.savedFilter.id, views: def.views }); console.log( ` Created: "${def.name}" (${result.ok.savedFilter.id}) — ${ops.length} ops, ${def.visibility}`, ); } else { console.error(` Failed to create "${def.name}"`); } } // Track views to populate view counts console.log(`👁️ Tracking views for ${createdFilters.length} filters...`); for (const filter of createdFilters) { for (let i = 0; i < filter.views; i++) { await trackSavedFilterView(filter.id); } console.log(` "${filter.name}": ${filter.views} views`); } console.log(` ✅ Seed complete! Credentials: Email: ${ownerEmail} Password: ${password} Navigate to: http://localhost:3000/${organization.slug}/${project.slug}/${target.slug}/insights http://localhost:3000/${organization.slug}/${project.slug}/${target.slug}/insights/manage-filters `); } main().catch(err => { console.error('Seed failed:', err); process.exit(1); });