( config: AnalyticsConfig, )
| 149 | } |
| 150 | |
| 151 | export const layer = ( |
| 152 | config: AnalyticsConfig, |
| 153 | ): Layer.Layer<Analytics, never, HttpClient.HttpClient | FileSystem.FileSystem> => |
| 154 | Layer.effect(Analytics)( |
| 155 | Effect.gen(function* () { |
| 156 | const disabled = config.disabled ?? isAnalyticsDisabled(); |
| 157 | if (disabled) return analyticsNoop; |
| 158 | |
| 159 | const http = yield* HttpClient.HttpClient; |
| 160 | const distinctId = yield* loadOrMintAnonymousId(config.dataDir); |
| 161 | const buffer = yield* Ref.make<ReadonlyArray<BufferedEvent>>([]); |
| 162 | |
| 163 | const posthogHost = config.posthogHost ?? DEFAULT_POSTHOG_HOST; |
| 164 | const posthogKey = config.posthogKey ?? DEFAULT_POSTHOG_KEY; |
| 165 | |
| 166 | const sendBatch = (events: ReadonlyArray<BufferedEvent>) => |
| 167 | HttpClientRequest.post(`${posthogHost}/batch/`).pipe( |
| 168 | HttpClientRequest.bodyJson({ |
| 169 | api_key: posthogKey, |
| 170 | batch: events.map((entry) => ({ |
| 171 | event: entry.event, |
| 172 | distinct_id: distinctId, |
| 173 | timestamp: entry.timestamp, |
| 174 | properties: { |
| 175 | ...entry.properties, |
| 176 | // Anonymous events only: never materialize a person profile. |
| 177 | $process_person_profile: false, |
| 178 | surface: config.surface, |
| 179 | channel: config.channel, |
| 180 | app_version: config.version, |
| 181 | }, |
| 182 | })), |
| 183 | }), |
| 184 | Effect.flatMap(http.execute), |
| 185 | Effect.flatMap(HttpClientResponse.filterStatusOk), |
| 186 | Effect.asVoid, |
| 187 | Effect.timeout(Duration.seconds(10)), |
| 188 | ); |
| 189 | |
| 190 | const flush: Effect.Effect<void> = Effect.gen(function* () { |
| 191 | while (true) { |
| 192 | const batch = yield* Ref.modify(buffer, (current) => { |
| 193 | const next = current.slice(0, FLUSH_BATCH_SIZE); |
| 194 | return [next, current.slice(next.length)] as const; |
| 195 | }); |
| 196 | if (batch.length === 0) return; |
| 197 | yield* sendBatch(batch).pipe( |
| 198 | // Delivery failed: put the batch back for the next cadence tick and |
| 199 | // stop draining (the endpoint is unreachable, later batches would |
| 200 | // fail the same way). |
| 201 | Effect.tapError(() => Ref.update(buffer, (current) => [...batch, ...current])), |
| 202 | ); |
| 203 | } |
| 204 | }).pipe( |
| 205 | Effect.catchCause((cause) => |
| 206 | Effect.logDebug("Analytics.flush failed").pipe(Effect.annotateLogs("cause", cause)), |
| 207 | ), |
| 208 | ); |
no test coverage detected