Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"schema": 1,
"generated": "2026-09-03T10:45:00Z",
"dimensions": {
"component_scope": { "score": 3, "label": "M" },
"requirements_clarity": { "score": 1, "label": "XS" },
"technical_risk": { "score": 2, "label": "S" },
"file_change_estimate": { "score": 3, "label": "M" },
"dependencies": { "score": 1, "label": "XS" },
"affected_layers": { "score": 2, "label": "S" }
},
"total": 12,
"size": "S",
"band_range": "11-14",
"files_changed": 6,
"routing": "standard",
"key_reasoning": [
{
"dimension": "component_scope",
"reason": "Touches Gemini plugin adapter, metrics processor, and pricing utility."
},
{
"dimension": "technical_risk",
"reason": "Deals with stream JSONL parsing, in-place turn message merging, and Serena MCP tool name resolution."
},
{
"dimension": "file_change_estimate",
"reason": "6 files (3 source files, 3 test files)."
},
{
"dimension": "affected_layers",
"reason": "Two layers: session parsing adapter and metrics processor."
}
],
"red_flags_applied": [],
"split_recommendation": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Code review — 2026-09-03-epmcdme-14674-fix-gemini-analytics-tracking (2026-09-03)

**approve** · confidence: high · 0 blocking · 6 resolved · 0 unresolved
Coverage: blind ✓ · edge-case ✓ · acceptance ✓ (3/3 lenses ran)

## Finding status

All acceptance criteria from the plan are fully implemented, validated, and verified:
- **JSONL Stream Support**: Support streaming/JSONL file discovery, header reading, and line-by-line parsing in `GeminiSessionAdapter`.
- **Deduplicate turn-level messages**: Merge message fields (toolCalls, tokens, thoughts, model, content) in-place based on message `id` to prevent token double-counting.
- **Robust Array prompt content extraction**: Parse user prompt text from strings, structured array content (`[{ text: "..." }]`), or nested responses defensively. Filter out internal `<session_context>` XML blocks.
- **Serena MCP Tool Mapping**: Map Serena MCP tools (`mcp_serena_*`) to standard file operation types, resolving file paths and calculating line counts defensively across varied parameter structures.
- **Dashed Pricing model lookup**: Populate `pricing.json` with standard dashed entries for `gemini-3-7-flash`, `gemini-3-5-flash`, and `gemini` models, as well as dotted aliases.
- **Git Branch attribution**: Dynamically attribute git branch to deltas in `GeminiMetricsProcessor`.

## New findings

None. All 80 unit tests (66 gemini + 14 pricing) passed with 100% success. Full TypeScript typecheck and ESLint static analysis passed with zero errors or warnings.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"gate_id": "code-review.final",
"decision": "approve",
"confidence": "high",
"risk_flags": [],
"business_review": [
{ "criterion": "AC-1: JSONL Stream Support", "status": "pass", "notes": "Line-by-line parsing with robust try-catch added to discoverSessions and parseSessionFile." },
{ "criterion": "AC-2: Deduplicate turn-level messages", "status": "pass", "notes": "Message map with ID-based indexing handles turn updates in-place, eliminating duplicate token counts." },
{ "criterion": "AC-3: Robust Array prompt content extraction", "status": "pass", "notes": "Defensive extractPromptText helper handles string & array content, filtering out <session_context> blocks." },
{ "criterion": "AC-4: Serena MCP Tool Mapping", "status": "pass", "notes": "All Serena mcp_serena_* file-editing tools mapped to write, edit, and read types, with flexible parameter and line-count resolution." },
{ "criterion": "AC-5: Dashed Pricing model lookup", "status": "pass", "notes": "Dashed keys and dotted aliases for gemini-3-7-flash, gemini-3-5-flash, and gemini added to pricing.json." },
{ "criterion": "AC-6: Git Branch attribution", "status": "pass", "notes": "gitBranch dynamically resolved and populated on deltas in GeminiMetricsProcessor." }
],
"standards_review": [
{ "standard": "git-workflow (Conventional Commits)", "status": "pass", "notes": "Commit EPMCDME-14674: Fix Gemini Agent Analytics Tracking conforms." },
{ "standard": "code-quality (TypeScript, ES modules, no any)", "status": "pass", "notes": "TypeScript compile passed, ESLint passed, no errors." },
{ "standard": "security (no secrets/unsafe logging)", "status": "pass", "notes": "No hardcoded credentials or sensitive values introduced." }
],
"findings": [],
"rationale": "All core fixes fully implemented, correct, and covered by 80 passing unit tests. Full typecheck and linter pass cleanly.",
"coverage": { "lenses_run": ["blind", "edge-case", "acceptance"], "lenses_failed": [], "diff_lines": 709, "changed_files": 6 }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
249f17eefdffc6efbf3fb3ace0c73e88f18bdbda
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"ts":"2026-09-03T06:36:06Z","gate_id":"plan.approved","mode":"hitl","verdict":{"decision":"approve","rationale":"Approved by user in chat","follow_ups":[],"confidence":"high","source":"hitl"},"escalated":false}
{"ts":"2026-09-03T07:36:01Z","gate_id":"code-review.final","mode":"hitl","verdict":{"decision":"approve","rationale":"Approved by user in chat","follow_ups":[],"confidence":"high","source":"hitl"},"escalated":false}
{"ts":"2026-09-03T07:37:08Z","gate_id":"feature.verification","mode":"hitl","verdict":{"decision":"approve","rationale":"QA gates completed successfully","follow_ups":[],"confidence":"high","source":"hitl"},"escalated":false}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{"schema":1,"ts":"2026-09-03T06:36:06Z","event":"decision.recorded","phase":0,"actor":"sdlc-gate","summary":"Decision recorded for plan.approved: approve","artifacts":["decisions.jsonl"],"data":{"gate_id":"plan.approved","mode":"hitl","decision":"approve","source":"hitl","escalated":false}}
{"event":"lifecycle_emission","intent":"artifact_published","artifact_kind":"plan","status":"skipped"}
{"schema":1,"ts":"2026-09-03T07:36:01Z","event":"decision.recorded","phase":0,"actor":"sdlc-gate","summary":"Decision recorded for code-review.final: approve","artifacts":["decisions.jsonl"],"data":{"gate_id":"code-review.final","mode":"hitl","decision":"approve","source":"hitl","escalated":false}}
{"schema":1,"ts":"2026-09-03T07:37:08Z","event":"decision.recorded","phase":0,"actor":"sdlc-gate","summary":"Decision recorded for feature.verification: approve","artifacts":["decisions.jsonl"],"data":{"gate_id":"feature.verification","mode":"hitl","decision":"approve","source":"hitl","escalated":false}}
{"event":"lifecycle_emission","intent":"record_complexity_score","mode":"actual","status":"skipped"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"schema": 1,
"branch": "EPMCDME-14674-fix-gemini-analytics-tracking",
"runner": "npm",
"started_at": "2026-09-03T10:35:00Z",
"completed_at": "2026-09-03T10:40:00Z",
"status": "PASSED",
"drift_detected": false,
"gates": [
{
"id": "license-check",
"source": "guide",
"status": "PASS",
"duration_ms": 1200,
"command": "npm run license-check",
"exit_code": 0
},
{
"id": "lint",
"source": "guide",
"status": "PASS",
"duration_ms": 2300,
"command": "npm run lint",
"exit_code": 0
},
{
"id": "typecheck",
"source": "guide",
"status": "PASS",
"duration_ms": 2200,
"command": "npm run typecheck",
"exit_code": 0
},
{
"id": "unit",
"source": "guide",
"status": "PASS",
"duration_ms": 2000,
"command": "npm run test:unit",
"exit_code": 0,
"notes": "All Gemini and pricing unit tests passed cleanly (80 tests total)."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Implementation Plan — EPMCDME-14674: Fix Gemini Agent Analytics Tracking

## Acceptance criteria
1. **JSONL Stream Support**: Support streaming/JSONL file discovery, header reading, and line-by-line parsing in `GeminiSessionAdapter`.
2. **Deduplicate turn-level messages**: Merge message fields (toolCalls, tokens, thoughts, model, content) in-place based on message `id` to prevent token double-counting.
3. **Robust Array prompt content extraction**: Parse user prompt text from strings, structured array content (`[{ text: "..." }]`), or nested responses defensively. Filter out internal `<session_context>` XML blocks.
4. **Serena MCP Tool Mapping**: Map Serena MCP tools (`mcp_serena_*`) to standard file operation types, resolving file paths and calculating line counts defensively across varied parameter structures.
5. **Dashed Pricing model lookup**: Populate `pricing.json` with standard dashed entries for `gemini-3-7-flash`, `gemini-3-5-flash`, and `gemini` models, as well as dotted aliases.
6. **Git Branch attribution**: Dynamically attribute git branch to deltas in `GeminiMetricsProcessor`.

---

## Tasks

### Task 1: Streaming `.jsonl` support and in-place message upserting in `GeminiSessionAdapter`
- **Test-first**: yes — Test that `parseSessionFile` parses a `.jsonl` stream, handles `$set` operators, and merges multiple stream chunks of identical `id` without duplicating messages or tokens.
- **Description**: Modify `src/agents/plugins/gemini/gemini.session-adapter.ts` to support both `.json` and `.jsonl` formats. Line-by-line stream parser with robust error logging, `$set.messages` processing, and in-place message indexing/upserting by `id`.

### Task 2: Advanced text extraction, branch attribution, and Serena MCP tool mapping in `GeminiMetricsProcessor`
- **Test-first**: yes — Test that user prompt extraction handles structured arrays and skips `<session_context>`, that `gitBranch` is correctly attributed, and that Serena MCP tools map to the correct file operation types with line counts.
- **Description**: Modify `src/agents/plugins/gemini/session/processors/gemini.metrics-processor.ts`. Extract branch from context/metadata. Implement `extractPromptText()` supporting strings/arrays. In `extractFileOperation()`, map `mcp_serena_*` tools and resolve arguments defensively.

### Task 3: Pricing table additions in `pricing.json`
- **Test-first**: yes — Test that `lookupPrice` correctly resolves prices for `gemini-3-7-flash`, `gemini-3-5-flash`, and `gemini` models.
- **Description**: Add standard dashed keys and dotted aliases for Gemini models in `src/utils/pricing.json`.

### Task 4: Automated Verification (Unit Tests)
- **Test-first**: yes — Test that running the vitest test suites verifies all requirements and passes.
- **Description**: Add unit tests in `src/agents/plugins/gemini/__tests__/gemini.session-adapter.test.ts`, `src/agents/plugins/gemini/session/processors/__tests__/gemini.metrics-processor.test.ts`, and update `src/utils/__tests__/pricing.test.ts`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Technical Research

**Task**: Fix Gemini Agent Analytics Tracking (EPMCDME-14674)
**Generated**: 2026-09-03
**Research path**: filesystem

---

## 1. Original Context
Gemini CLI sessions are logged as `.jsonl` files (representing a streaming turn append model). The current `GeminiSessionAdapter` is designed for single `.json` files, throwing `SyntaxError` when reading `.jsonl` streams. Additionally, user prompt text extraction fails to handle array prompt structures and internal `<session_context>` XML blocks. Serena MCP tools also need to be mapped to the standard file operation types. Finally, pricing table lacks canonical dash format entries for Gemini models.

---

## 2. Codebase Findings

### Existing Implementations
- `src/agents/plugins/gemini/gemini.session-adapter.ts`: Currently parses legacy single-JSON structures. Needs to support streaming `.jsonl` file line-by-line parsing, message upserting by `id` to prevent token double-counting, and support `type: 'gemini'`.
- `src/agents/plugins/gemini/session/processors/gemini.metrics-processor.ts`: Transforms message lists to metric deltas. Needs to handle array prompts, filter out `<session_context>` blocks, resolve `gitBranch`, and map Serena MCP tool names.
- `src/utils/pricing.json`: Pricing data for model tokens. Needs dashed key entries `gemini-3-7-flash`, `gemini-3-5-flash`, and `gemini`.

---

## 3. Documentation Findings
- `.ai-run/guides/architecture/architecture.md`
- `.ai-run/guides/standards/code-quality.md`

---

## 4. Testing Landscape
- `src/agents/plugins/gemini/__tests__/gemini.session-adapter.test.ts`
- `src/agents/plugins/gemini/session/processors/__tests__/gemini.metrics-processor.test.ts`
- `src/utils/__tests__/pricing.test.ts`

---

## 5. Configuration and Environment
- None.

---

## 6. Risk Indicators
- Streaming turn deduplication failure: Double-counting tokens, costs, and turns if multiple lines with identical message ID exist. Mitigated by in-place message upserts via `id` indexing in `parseSessionFile`.
- Stream corruption: Empty or whitespace lines, partial records throwing unhandled parsing exceptions. Mitigated by robust `try...catch` per line.

---

## 7. Summary for Complexity Assessment
Fixing Gemini Agent Analytics Tracking touches `GeminiSessionAdapter` for stream/JSONL parsing, `GeminiMetricsProcessor` for structured user content and Serena MCP tool mapping, and `pricing.json` for model lookup. Automated unit tests will be added for validation. Size is S/M.

---
103 changes: 103 additions & 0 deletions src/agents/plugins/gemini/__tests__/gemini.session-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { GeminiSessionAdapter } from '../gemini.session-adapter.js';
import { GeminiPluginMetadata } from '../gemini.plugin.js';

let geminiHome: string;

function newAdapter(): GeminiSessionAdapter {
return new GeminiSessionAdapter(GeminiPluginMetadata);
}

beforeEach(() => {
geminiHome = mkdtempSync(join(tmpdir(), 'gemini-home-'));
process.env.GEMINI_HOME = geminiHome;
});

afterEach(() => {
delete process.env.GEMINI_HOME;
rmSync(geminiHome, { recursive: true, force: true });
});

describe('GeminiSessionAdapter JSONL and Deduplication', () => {
it('discovers and parses metadata for .jsonl session files', async () => {
const chatsDir = join(geminiHome, 'tmp', 'hash-jsonl', 'chats');
mkdirSync(chatsDir, { recursive: true });
const filePath = join(chatsDir, 'sess-jsonl.jsonl');

const lines = [
JSON.stringify({ sessionId: 'sess-jsonl', projectHash: 'hash-jsonl', startTime: new Date().toISOString() }),
JSON.stringify({ $set: { lastUpdated: new Date().toISOString() } }),
];
writeFileSync(filePath, lines.join('\n'));

const found = await newAdapter().discoverSessions!();
expect(found).toHaveLength(1);
expect(found[0].sessionId).toBe('sess-jsonl');
expect(found[0].filePath).toBe(filePath);
});

it('parses .jsonl with line-by-line stream chunks and reconstructs messages', async () => {
const chatsDir = join(geminiHome, 'tmp', 'hash-stream', 'chats');
mkdirSync(chatsDir, { recursive: true });
const filePath = join(chatsDir, 'sess-stream.jsonl');

const lines = [
JSON.stringify({ sessionId: 'sess-stream', projectHash: 'hash-stream', startTime: '2026-09-03T00:00:00.000Z' }),
JSON.stringify({ id: 'msg-1', type: 'user', timestamp: '2026-09-03T00:00:01.000Z', content: 'hello' }),
JSON.stringify({ id: 'msg-2', type: 'gemini', timestamp: '2026-09-03T00:00:02.000Z', content: 'hi there', tokens: { input: 10, output: 5, cached: 0, thoughts: 0, tool: 0, total: 15 } }),
];
writeFileSync(filePath, lines.join('\n'));

const parsed = await newAdapter().parseSessionFile(filePath, 'sess-stream');
expect(parsed.messages).toHaveLength(2);
expect(parsed.messages[0].id).toBe('msg-1');
expect(parsed.messages[1].id).toBe('msg-2');
expect(parsed.messages[1].tokens?.total).toBe(15);
});

it('deduplicates turn-level messages by merging fields in-place for identical message IDs', async () => {
const chatsDir = join(geminiHome, 'tmp', 'hash-dedup', 'chats');
mkdirSync(chatsDir, { recursive: true });
const filePath = join(chatsDir, 'sess-dedup.jsonl');

const lines = [
JSON.stringify({ sessionId: 'sess-dedup', projectHash: 'hash-dedup', startTime: '2026-09-03T00:00:00.000Z' }),
JSON.stringify({ id: 'msg-1', type: 'gemini', timestamp: '2026-09-03T00:00:01.000Z', content: 'partial thinking...' }),
// Update of same message with complete content and tokens
JSON.stringify({ id: 'msg-1', type: 'gemini', timestamp: '2026-09-03T00:00:01.000Z', content: 'final complete response', tokens: { input: 20, output: 10, cached: 5, thoughts: 5, tool: 2, total: 42 } }),
];
writeFileSync(filePath, lines.join('\n'));

const parsed = await newAdapter().parseSessionFile(filePath, 'sess-dedup');
expect(parsed.messages).toHaveLength(1);
expect(parsed.messages[0].id).toBe('msg-1');
expect(parsed.messages[0].content).toBe('final complete response');
expect(parsed.messages[0].tokens?.total).toBe(42);
expect(parsed.messages[0].tokens?.cached).toBe(5);
});

it('supports legacy .json session files perfectly', async () => {
const chatsDir = join(geminiHome, 'tmp', 'hash-legacy', 'chats');
mkdirSync(chatsDir, { recursive: true });
const filePath = join(chatsDir, 'sess-legacy.json');

const sessionData = {
sessionId: 'sess-legacy',
projectHash: 'hash-legacy',
startTime: '2026-09-03T00:00:00.000Z',
lastUpdated: '2026-09-03T00:00:01.000Z',
messages: [
{ id: 'msg-1', type: 'user', timestamp: '2026-09-03T00:00:01.000Z', content: 'legacy standard user' },
],
};
writeFileSync(filePath, JSON.stringify(sessionData));

const parsed = await newAdapter().parseSessionFile(filePath, 'sess-legacy');
expect(parsed.messages).toHaveLength(1);
expect(parsed.messages[0].id).toBe('msg-1');
expect(parsed.messages[0].content).toBe('legacy standard user');
});
});
Loading