Skip to content

Commit 9b455ed

Browse files
authored
[OPIK-5692] [BE][FE] feat: add agent config & runner analytics events (#6338)
* [OPIK-5692] [BE][FE] feat: add agent config & runner analytics events Add PostHog instrumentation for agent configs and sandbox runner to track Opik 2.0 launch metrics defined in OPIK-5245. Backend (3 events via AnalyticsService): - opik_agent_config_saved: fired in createAgentConfig() and updateAgentConfig() to capture both UI and SDK config creation. - opik_agent_config_deployed: fired in createOrUpdateEnvs() and setEnvByBlueprintName() to capture all deployment paths. - opik_sandbox_job_created: fired in createJob() to capture runner adoption across UI and API. Frontend (1 event via trackEvent): - opik_agent_config_ui_deployed: fired from DeployToPopover with UI-specific context (is_new_prod) not available at the BE endpoint. All BE calls wrapped in try/catch to avoid disrupting primary operations. User identity resolved automatically via the updated AnalyticsService.resolveIdentity(). * fix(analytics): address Baz review — dedup deploy helpers and fix blueprint_id - Consolidated trackAgentConfigDeployed and trackAgentConfigDeployedByName into a shared trackAgentConfigDeployedEvent helper (dedup) - Changed blueprint_id to collect all distinct IDs instead of findFirst(), avoiding mis-attribution when envs reference different blueprints * fix(analytics): add opik_ prefix to event names and fix blueprint_id type inconsistency Address PR review comments: prefix all analytics events with opik_ per instrumentation guidelines, and separate blueprint_id (UUID) from blueprint_name (display name) to prevent type mixing in deployed events. Also update the analytics skill to document property naming conventions. * fix(analytics): address Boris's PR review comments - Remove try/catch wrappers around analytics calls (fire-and-forget) - Add workspace_id to all analytics events - Emit one event per env instead of comma-separated aggregation - Track implicit prod deployment on first config creation - Soften SKILL.md _id naming rule (thread_id is human-readable) * chore: revert unrelated package-lock.json changes * fix(analytics): move tracking to service layer for consistent event properties Move analytics tracking from AgentConfigsResource to AgentConfigServiceImpl so both deploy paths (by-ID and by-name) emit consistent event properties (always blueprint_id + blueprint_name). * fix(analytics): coerce deployed_to_prod to string in frontend Aligns frontend event type with backend String.valueOf() serialization so deployed_to_prod is consistently a string across all emitters. * fix(analytics): move sandbox_job_created tracking to service layer Move opik_sandbox_job_created event from LocalRunnersResource to EndpointJobServiceImpl for consistency with agent config tracking. * fix: add missing AnalyticsService param to test constructors * fix: use resolved projectId in analytics callbacks to avoid NPE Move doOnNext callbacks inside flatMap scope so they use the resolved projectId variable instead of blueprint.projectId() which is null. Verified with 66 passing AgentConfigsResourceTest tests.
1 parent 7e3d4ec commit 9b455ed

9 files changed

Lines changed: 75 additions & 14 deletions

File tree

.agents/skills/analytics-instrumentation/SKILL.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private final @NonNull AnalyticsService analyticsService;
5959

6060
2. Call `trackEvent`:
6161
```java
62-
analyticsService.trackEvent("onboarding_first_trace",
62+
analyticsService.trackEvent("opik_onboarding_first_trace",
6363
Map.of("trace_id", traceId, "project_id", projectId));
6464
```
6565

@@ -89,6 +89,12 @@ Backend events: Java → comet-stats → Segment → PostHog
8989
PostHog native: Browser → posthog-js → PostHog (pageviews, feature flags, identification)
9090
```
9191

92+
## Event Property Conventions
93+
94+
- **Consistent typing per property**: A given property key should always carry the same kind of value. Don't pass a UUID in one code path and a human-readable name in another for the same key.
95+
- **Separate ID and name properties**: When both a UUID and a display name exist, use distinct keys (e.g. `blueprint_id` for the UUID, `blueprint_name` for the display name). If one is unavailable in a code path, omit the key or send an empty string — don't repurpose the other key.
96+
- **Include `workspace_id`**: All backend analytics events should include the workspace ID for segmentation.
97+
9298
## Deciding Frontend vs Backend
9399

94100
- **Frontend**: UI interactions (button clicks, wizard steps, form submissions, page visits)

apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AgentConfigsResource.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.comet.opik.domain.AgentBlueprint;
1010
import com.comet.opik.domain.AgentConfig;
1111
import com.comet.opik.domain.AgentConfigService;
12-
import com.comet.opik.infrastructure.auth.RequestContext;
1312
import com.fasterxml.jackson.annotation.JsonView;
1413
import io.swagger.v3.oas.annotations.Operation;
1514
import io.swagger.v3.oas.annotations.headers.Header;
@@ -19,7 +18,6 @@
1918
import io.swagger.v3.oas.annotations.responses.ApiResponse;
2019
import io.swagger.v3.oas.annotations.tags.Tag;
2120
import jakarta.inject.Inject;
22-
import jakarta.inject.Provider;
2321
import jakarta.validation.Valid;
2422
import jakarta.validation.constraints.Min;
2523
import jakarta.validation.constraints.NotNull;
@@ -55,7 +53,6 @@
5553
public class AgentConfigsResource {
5654

5755
private final @NonNull AgentConfigService agentConfigService;
58-
private final @NonNull Provider<RequestContext> requestContext;
5956

6057
@POST
6158
@Path("/blueprints")

apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/LocalRunnersResource.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ public Response createJob(
181181
String workspaceId = requestContext.get().getWorkspaceId();
182182
String userName = requestContext.get().getUserName();
183183
UUID jobId = endpointJobService.createJob(workspaceId, userName, request);
184+
184185
var uri = uriInfo.getAbsolutePathBuilder().path("/{jobId}").build(jobId);
185186
return Response.created(uri).build();
186187
}

apps/opik-backend/src/main/java/com/comet/opik/domain/AgentConfigService.java

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.comet.opik.api.error.ErrorMessage;
88
import com.comet.opik.api.validation.HasProjectIdentifier;
99
import com.comet.opik.infrastructure.AgentConfigConfiguration;
10+
import com.comet.opik.infrastructure.bi.AnalyticsService;
1011
import com.comet.opik.infrastructure.lock.LockService;
1112
import com.comet.opik.utils.JsonUtils;
1213
import com.comet.opik.utils.WorkspaceUtils;
@@ -82,6 +83,7 @@ class AgentConfigServiceImpl implements AgentConfigService {
8283
private final @NonNull ProjectService projectService;
8384
private final @NonNull LockService lockService;
8485
private final @NonNull AgentConfigConfiguration agentConfigConfiguration;
86+
private final @NonNull AnalyticsService analyticsService;
8587

8688
@Override
8789
public Mono<AgentBlueprint> createConfig(@NonNull AgentConfigCreate request) {
@@ -127,7 +129,12 @@ public Mono<AgentBlueprint> createConfig(@NonNull AgentConfigCreate request) {
127129

128130
return blueprint;
129131
})).subscribeOn(Schedulers.boundedElastic()),
130-
agentConfigConfiguration.getBlueprintLockDuration().toJavaDuration()));
132+
agentConfigConfiguration.getBlueprintLockDuration().toJavaDuration())
133+
.doOnNext(blueprint -> {
134+
trackAgentConfigSaved(workspaceId, projectId, blueprint);
135+
trackAgentConfigDeployed(workspaceId, projectId,
136+
blueprint.id(), String.valueOf(blueprint.name()), "prod");
137+
}));
131138
}
132139

133140
@Override
@@ -155,7 +162,8 @@ public Mono<AgentBlueprint> updateConfig(@NonNull AgentConfigCreate request) {
155162
return createBlueprint(dao, request, existingConfig.id(), projectId, workspaceId,
156163
userName);
157164
})).subscribeOn(Schedulers.boundedElastic()),
158-
agentConfigConfiguration.getBlueprintLockDuration().toJavaDuration()));
165+
agentConfigConfiguration.getBlueprintLockDuration().toJavaDuration())
166+
.doOnNext(blueprint -> trackAgentConfigSaved(workspaceId, projectId, blueprint)));
159167
}
160168

161169
@Override
@@ -494,7 +502,13 @@ public Mono<Void> createOrUpdateEnvs(@NonNull AgentConfigEnvUpdate request) {
494502
upsertEnvs(dao, workspaceId, projectId, userName, request.envs());
495503

496504
return null;
497-
})).subscribeOn(Schedulers.boundedElastic()));
505+
})).subscribeOn(Schedulers.boundedElastic()))
506+
.doOnSuccess(v -> {
507+
for (var env : request.envs()) {
508+
trackAgentConfigDeployed(workspaceId, projectId,
509+
env.blueprintId(), "", env.envName());
510+
}
511+
});
498512
}
499513

500514
@Override
@@ -506,9 +520,9 @@ public Mono<Void> setEnvByBlueprintName(@NonNull UUID projectId, @NonNull String
506520
log.info("Setting environment '{}' to blueprint '{}' for project '{}' in workspace '{}'",
507521
envName, blueprintName, projectId, workspaceId);
508522

509-
return lockService.<Void>executeWithLock(
523+
return lockService.<UUID>executeWithLock(
510524
new LockService.Lock(ENV_LOCK_FORMAT.formatted(workspaceId, projectId)),
511-
Mono.<Void>fromRunnable(() -> transactionTemplate.inTransaction(WRITE, handle -> {
525+
Mono.fromCallable(() -> transactionTemplate.inTransaction(WRITE, handle -> {
512526
AgentConfigDAO dao = handle.attach(AgentConfigDAO.class);
513527

514528
AgentBlueprint blueprint = requireBlueprintByName(dao, workspaceId, projectId, blueprintName);
@@ -519,8 +533,11 @@ public Mono<Void> setEnvByBlueprintName(@NonNull UUID projectId, @NonNull String
519533
.blueprintId(blueprint.id())
520534
.build()));
521535

522-
return null;
523-
})).subscribeOn(Schedulers.boundedElastic()));
536+
return blueprint.id();
537+
})).subscribeOn(Schedulers.boundedElastic()))
538+
.doOnNext(blueprintId -> trackAgentConfigDeployed(workspaceId, projectId,
539+
blueprintId, blueprintName, envName))
540+
.then();
524541
}
525542

526543
private void upsertEnvs(AgentConfigDAO dao, String workspaceId, UUID projectId, String userName,
@@ -676,4 +693,23 @@ public Mono<AgentBlueprint> createBlueprintFromMask(@NonNull UUID projectId, @No
676693

677694
return updateConfig(request);
678695
}
696+
697+
private void trackAgentConfigSaved(String workspaceId, UUID projectId, AgentBlueprint blueprint) {
698+
analyticsService.trackEvent("opik_agent_config_saved", Map.of(
699+
"workspace_id", workspaceId,
700+
"project_id", projectId.toString(),
701+
"blueprint_id", blueprint.id().toString(),
702+
"blueprint_name", String.valueOf(blueprint.name())));
703+
}
704+
705+
private void trackAgentConfigDeployed(String workspaceId, UUID projectId,
706+
UUID blueprintId, String blueprintName, String envName) {
707+
analyticsService.trackEvent("opik_agent_config_deployed", Map.of(
708+
"workspace_id", workspaceId,
709+
"project_id", projectId.toString(),
710+
"blueprint_id", blueprintId.toString(),
711+
"blueprint_name", blueprintName,
712+
"environment", envName,
713+
"deployed_to_prod", String.valueOf("prod".equalsIgnoreCase(envName))));
714+
}
679715
}

apps/opik-backend/src/main/java/com/comet/opik/domain/EndpointJobService.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.comet.opik.api.runner.LocalRunnerLogEntry;
1111
import com.comet.opik.api.runner.RunnerType;
1212
import com.comet.opik.infrastructure.LocalRunnerConfig;
13+
import com.comet.opik.infrastructure.bi.AnalyticsService;
1314
import com.comet.opik.infrastructure.redis.StringRedisClient;
1415
import com.comet.opik.utils.JsonUtils;
1516
import com.fasterxml.jackson.databind.JsonNode;
@@ -130,16 +131,18 @@ static String runnerCancellationsKey(UUID runnerId) {
130131
private final @NonNull IdGenerator idGenerator;
131132
private final @NonNull RunnerService runnerService;
132133
private final @NonNull LocalRunnerConfig runnerConfig;
134+
private final @NonNull AnalyticsService analyticsService;
133135

134136
@Inject
135137
EndpointJobServiceImpl(@NonNull StringRedisClient redisClient, @NonNull RedissonReactiveClient reactiveRedisClient,
136138
@NonNull IdGenerator idGenerator, @NonNull RunnerService runnerService,
137-
@NonNull LocalRunnerConfig runnerConfig) {
139+
@NonNull LocalRunnerConfig runnerConfig, @NonNull AnalyticsService analyticsService) {
138140
this.redisClient = redisClient;
139141
this.reactiveRedisClient = reactiveRedisClient;
140142
this.idGenerator = idGenerator;
141143
this.runnerService = runnerService;
142144
this.runnerConfig = runnerConfig;
145+
this.analyticsService = analyticsService;
143146
}
144147

145148
@Override
@@ -239,6 +242,12 @@ public UUID createJob(@NonNull String workspaceId, @NonNull String userName,
239242
RScoredSortedSet<String> runnerJobs = redisClient.getScoredSortedSet(runnerJobsKey(runnerId));
240243
runnerJobs.add(Instant.now().toEpochMilli(), jobId.toString());
241244

245+
analyticsService.trackEvent("opik_sandbox_job_created", Map.of(
246+
"workspace_id", workspaceId,
247+
"project_id", request.projectId().toString(),
248+
"job_id", jobId.toString(),
249+
"agent_name", request.agentName()));
250+
242251
return jobId;
243252
}
244253

apps/opik-backend/src/test/java/com/comet/opik/domain/LocalRunnerReaperIntegrationTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.comet.opik.api.runner.RunnerType;
1010
import com.comet.opik.infrastructure.LocalRunnerConfig;
1111
import com.comet.opik.infrastructure.auth.RequestContext;
12+
import com.comet.opik.infrastructure.bi.AnalyticsService;
1213
import com.comet.opik.infrastructure.redis.StringRedisClient;
1314
import com.redis.testcontainers.RedisContainer;
1415
import io.dropwizard.util.Duration;
@@ -95,7 +96,7 @@ void setUp() {
9596
runnerService = new RunnerServiceImpl(stringRedis, idGenerator, projectService, runnerConfig,
9697
() -> endpointJobService, () -> connectBridgeService, () -> requestContext);
9798
endpointJobService = new EndpointJobServiceImpl(stringRedis, redisClient.reactive(), idGenerator,
98-
runnerService, runnerConfig);
99+
runnerService, runnerConfig, Mockito.mock(AnalyticsService.class));
99100
}
100101

101102
@BeforeEach

apps/opik-backend/src/test/java/com/comet/opik/domain/LocalRunnerServiceImplTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.comet.opik.api.runner.RunnerType;
1919
import com.comet.opik.infrastructure.LocalRunnerConfig;
2020
import com.comet.opik.infrastructure.auth.RequestContext;
21+
import com.comet.opik.infrastructure.bi.AnalyticsService;
2122
import com.comet.opik.infrastructure.redis.StringRedisClient;
2223
import com.fasterxml.jackson.databind.ObjectMapper;
2324
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -112,7 +113,7 @@ void setUp() {
112113
runnerService = new RunnerServiceImpl(stringRedis, idGenerator, projectService, runnerConfig,
113114
() -> endpointJobService, () -> connectBridgeService, () -> requestContext);
114115
endpointJobService = new EndpointJobServiceImpl(stringRedis, redisClient.reactive(), idGenerator,
115-
runnerService, runnerConfig);
116+
runnerService, runnerConfig, Mockito.mock(AnalyticsService.class));
116117
connectBridgeService = new ConnectBridgeServiceImpl(stringRedis, idGenerator, runnerService, runnerConfig);
117118
}
118119

apps/opik-frontend/src/lib/analytics/tracking.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const OpikEvent = {
55
ONBOARDING_FIRST_TRACE_RECEIVED: "opik_onboarding_first_trace_received",
66
ONBOARDING_SKIPPED: "opik_onboarding_skipped",
77
EVAL_SUITE_UI_CONFIGURED: "opik_eval_suite_ui_configured",
8+
AGENT_CONFIG_UI_DEPLOYED: "opik_agent_config_ui_deployed",
89
} as const;
910

1011
type OpikEventValues = (typeof OpikEvent)[keyof typeof OpikEvent];

apps/opik-frontend/src/v2/pages/AgentConfigurationPage/AgentConfigurationTab/DeployToPopover.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
isProdTag,
1818
isStageTag,
1919
} from "@/utils/agent-configurations";
20+
import { OpikEvent, trackEvent } from "@/lib/analytics/tracking";
2021

2122
type DeployToPopoverProps = {
2223
item: ConfigHistoryItem;
@@ -93,6 +94,14 @@ const DeployToPopover: React.FC<DeployToPopoverProps> = ({
9394
});
9495
}
9596

97+
trackEvent(OpikEvent.AGENT_CONFIG_UI_DEPLOYED, {
98+
project_id: projectId,
99+
blueprint_id: item.id,
100+
environments: selectedStages,
101+
deployed_to_prod: String(prodSelected),
102+
is_new_prod: isNewProd,
103+
});
104+
96105
setOpen(false);
97106
};
98107

0 commit comments

Comments
 (0)