(job: Job<ImportQueuePayload>)
| 69 | } |
| 70 | |
| 71 | export async function importJob(job: Job<ImportQueuePayload>) { |
| 72 | const { importId } = job.data.payload; |
| 73 | |
| 74 | const record = await db.import.findUniqueOrThrow({ |
| 75 | where: { id: importId }, |
| 76 | include: { project: true }, |
| 77 | }); |
| 78 | |
| 79 | const jobLogger = logger.child({ importId, config: record.config }); |
| 80 | jobLogger.info('Starting import job'); |
| 81 | |
| 82 | const providerInstance = createProvider(record, jobLogger); |
| 83 | const shouldGenerateSessionIds = providerInstance.shouldGenerateSessionIds(); |
| 84 | |
| 85 | try { |
| 86 | const isRetry = record.currentStep !== null; |
| 87 | const canResume = |
| 88 | isRetry && RESUMABLE_STEPS.includes(record.currentStep as string); |
| 89 | |
| 90 | // ------------------------------------------------------- |
| 91 | // STAGING PHASE: clean slate on failure, run from scratch |
| 92 | // ------------------------------------------------------- |
| 93 | if (!canResume) { |
| 94 | if (isRetry) { |
| 95 | jobLogger.info( |
| 96 | 'Retry detected before resumable phase — cleaning staging data' |
| 97 | ); |
| 98 | await cleanupStagingData(importId); |
| 99 | } |
| 100 | |
| 101 | // Phase 1: Load events into staging |
| 102 | await updateImportStatus(jobLogger, job, importId, { step: 'loading' }); |
| 103 | |
| 104 | const totalEvents = await providerInstance |
| 105 | .getTotalEventsCount() |
| 106 | .catch(() => -1); |
| 107 | let processedEvents = 0; |
| 108 | const eventBatch: IClickhouseEvent[] = []; |
| 109 | |
| 110 | // Profiles derived inline from events (e.g. Amplitude, which has no |
| 111 | // profile export API). A bounded dedup map keeps memory flat regardless of |
| 112 | // event volume; the profiles table's ReplacingMergeTree(last_seen_at) |
| 113 | // collapses any duplicate rows that span flush boundaries to the latest |
| 114 | // activity per id. |
| 115 | const canProfileFromEvents = |
| 116 | typeof (providerInstance as AmplitudeProvider) |
| 117 | .transformEventToProfile === 'function'; |
| 118 | const PROFILE_MAP_CAP = 50_000; |
| 119 | const profileMap = new Map<string, IClickhouseProfile>(); |
| 120 | let processedProfiles = 0; |
| 121 | |
| 122 | const flushProfiles = async () => { |
| 123 | if (profileMap.size === 0) { |
| 124 | return; |
| 125 | } |
| 126 | const values = Array.from(profileMap.values()); |
| 127 | await insertProfilesBatch(values, record.projectId); |
| 128 | processedProfiles += values.length; |
nothing calls this directly
no test coverage detected