@@ -43,6 +43,10 @@ import {
4343} from "../../persistence/Layers/Sqlite.ts" ;
4444import { ServerSettingsService } from "../../serverSettings.ts" ;
4545import { AnalyticsServiceNoopLive } from "../../telemetry/Layers/AnalyticsService.ts" ;
46+ import {
47+ AnalyticsService ,
48+ type AnalyticsServiceShape ,
49+ } from "../../telemetry/Services/AnalyticsService.ts" ;
4650
4751const defaultServerSettingsLayer = ServerSettingsService . layerTest ( ) ;
4852
@@ -242,6 +246,12 @@ const hasMetricSnapshot = (
242246 Object . entries ( attributes ) . every ( ( [ key , value ] ) => snapshot . attributes ?. [ key ] === value ) ,
243247 ) ;
244248
249+ interface AnalyticsRecord {
250+ readonly event : string ;
251+ readonly properties ?: Record < string , unknown > | undefined ;
252+ readonly options ?: Parameters < AnalyticsServiceShape [ "record" ] > [ 2 ] | undefined ;
253+ }
254+
245255function makeProviderServiceLayer ( ) {
246256 const codex = makeFakeCodexAdapter ( ) ;
247257 const claude = makeFakeCodexAdapter ( "claudeAgent" ) ;
@@ -382,7 +392,7 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se
382392 state : "completed" ,
383393 } ,
384394 } ) ;
385- yield * sleep ( 20 ) ;
395+ yield * sleep ( 100 ) ;
386396 } ) . pipe ( Effect . provide ( providerLayer ) ) ;
387397
388398 assert . equal ( canonicalEvents . length , 1 ) ;
@@ -391,6 +401,83 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se
391401 } ) . pipe ( Effect . provide ( NodeServices . layer ) ) ,
392402) ;
393403
404+ it . effect ( "ProviderServiceLive records provider turns as one duration product span" , ( ) =>
405+ Effect . gen ( function * ( ) {
406+ const codex = makeFakeCodexAdapter ( ) ;
407+ const analyticsRecords : AnalyticsRecord [ ] = [ ] ;
408+ const registry : typeof ProviderAdapterRegistry . Service = {
409+ getByProvider : ( provider ) =>
410+ provider === "codex"
411+ ? Effect . succeed ( codex . adapter )
412+ : Effect . fail ( new ProviderUnsupportedError ( { provider } ) ) ,
413+ listProviders : ( ) => Effect . succeed ( [ "codex" ] ) ,
414+ } ;
415+ const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive . pipe (
416+ Layer . provide ( SqlitePersistenceMemory ) ,
417+ ) ;
418+ const directoryLayer = ProviderSessionDirectoryLive . pipe ( Layer . provide ( runtimeRepositoryLayer ) ) ;
419+ const analyticsLayer = Layer . succeed ( AnalyticsService , {
420+ record : ( event , properties , options ) =>
421+ Effect . sync ( ( ) => {
422+ analyticsRecords . push ( { event, properties, options } ) ;
423+ } ) ,
424+ flush : Effect . void ,
425+ } ) ;
426+ const providerLayer = makeProviderServiceLive ( ) . pipe (
427+ Layer . provide ( Layer . succeed ( ProviderAdapterRegistry , registry ) ) ,
428+ Layer . provide ( directoryLayer ) ,
429+ Layer . provide ( defaultServerSettingsLayer ) ,
430+ Layer . provide ( analyticsLayer ) ,
431+ ) ;
432+
433+ yield * Effect . gen ( function * ( ) {
434+ const provider = yield * ProviderService ;
435+ yield * sleep ( 10 ) ;
436+ const threadId = asThreadId ( "thread-turn-analytics" ) ;
437+ yield * provider . startSession ( threadId , {
438+ provider : "codex" ,
439+ threadId,
440+ runtimeMode : "full-access" ,
441+ } ) ;
442+ const turn = yield * provider . sendTurn ( {
443+ threadId,
444+ input : "hello" ,
445+ interactionMode : "default" ,
446+ } ) ;
447+ codex . emit ( {
448+ eventId : asEventId ( "evt-turn-analytics-completed" ) ,
449+ provider : "codex" ,
450+ threadId,
451+ turnId : turn . turnId ,
452+ createdAt : new Date ( Date . now ( ) + 250 ) . toISOString ( ) ,
453+ type : "turn.completed" ,
454+ payload : {
455+ state : "completed" ,
456+ } ,
457+ } ) ;
458+ yield * sleep ( 100 ) ;
459+ } ) . pipe ( Effect . provide ( providerLayer ) ) ;
460+
461+ const turnRecord = analyticsRecords . find ( ( record ) => record . event === "marcode.provider.turn" ) ;
462+
463+ assert . isDefined ( turnRecord ) ;
464+ assert . isUndefined (
465+ analyticsRecords . find ( ( record ) => record . event === "marcode.provider.turn.sent" ) ,
466+ ) ;
467+ assert . isUndefined (
468+ analyticsRecords . find ( ( record ) => record . event === "marcode.provider.turn.completed" ) ,
469+ ) ;
470+ assert . equal ( turnRecord ?. properties ?. provider , "codex" ) ;
471+ assert . equal ( turnRecord ?. properties ?. outcome , "success" ) ;
472+ assert . equal ( turnRecord ?. properties ?. interaction_mode , "default" ) ;
473+ assert . isAtLeast ( Number ( turnRecord ?. options ?. durationMs ?? 0 ) , 1 ) ;
474+ assert . deepEqual (
475+ turnRecord ?. options ?. spanEvents ?. map ( ( event ) => event . name ) ,
476+ [ "marcode.provider.turn.sent" , "marcode.provider.turn.completed" ] ,
477+ ) ;
478+ } ) . pipe ( Effect . provide ( NodeServices . layer ) ) ,
479+ ) ;
480+
394481it . effect ( "ProviderServiceLive keeps persisted resumable sessions on startup" , ( ) =>
395482 Effect . gen ( function * ( ) {
396483 const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "marcode-provider-service-" ) ) ;
0 commit comments