devin-metrics 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. devin_metrics-0.2.0/LICENSE +21 -0
  2. devin_metrics-0.2.0/PKG-INFO +210 -0
  3. devin_metrics-0.2.0/README.md +196 -0
  4. devin_metrics-0.2.0/pyproject.toml +29 -0
  5. devin_metrics-0.2.0/setup.cfg +4 -0
  6. devin_metrics-0.2.0/src/devin_metrics/__init__.py +1 -0
  7. devin_metrics-0.2.0/src/devin_metrics/aggregate.py +199 -0
  8. devin_metrics-0.2.0/src/devin_metrics/churn.py +141 -0
  9. devin_metrics-0.2.0/src/devin_metrics/cli.py +246 -0
  10. devin_metrics-0.2.0/src/devin_metrics/collect.py +240 -0
  11. devin_metrics-0.2.0/src/devin_metrics/dashboard/__init__.py +1 -0
  12. devin_metrics-0.2.0/src/devin_metrics/dashboard/cli.py +137 -0
  13. devin_metrics-0.2.0/src/devin_metrics/dashboard/collect.py +415 -0
  14. devin_metrics-0.2.0/src/devin_metrics/dashboard/render.py +287 -0
  15. devin_metrics-0.2.0/src/devin_metrics/paths.py +81 -0
  16. devin_metrics-0.2.0/src/devin_metrics/render.py +170 -0
  17. devin_metrics-0.2.0/src/devin_metrics.egg-info/PKG-INFO +210 -0
  18. devin_metrics-0.2.0/src/devin_metrics.egg-info/SOURCES.txt +33 -0
  19. devin_metrics-0.2.0/src/devin_metrics.egg-info/dependency_links.txt +1 -0
  20. devin_metrics-0.2.0/src/devin_metrics.egg-info/entry_points.txt +3 -0
  21. devin_metrics-0.2.0/src/devin_metrics.egg-info/requires.txt +4 -0
  22. devin_metrics-0.2.0/src/devin_metrics.egg-info/top_level.txt +1 -0
  23. devin_metrics-0.2.0/tests/test_aggregate.py +161 -0
  24. devin_metrics-0.2.0/tests/test_churn.py +73 -0
  25. devin_metrics-0.2.0/tests/test_cli.py +84 -0
  26. devin_metrics-0.2.0/tests/test_collect.py +81 -0
  27. devin_metrics-0.2.0/tests/test_dashboard_cli.py +76 -0
  28. devin_metrics-0.2.0/tests/test_dashboard_collect.py +115 -0
  29. devin_metrics-0.2.0/tests/test_dashboard_render.py +73 -0
  30. devin_metrics-0.2.0/tests/test_dashboard_smoke.py +4 -0
  31. devin_metrics-0.2.0/tests/test_offline_core.py +76 -0
  32. devin_metrics-0.2.0/tests/test_paths.py +29 -0
  33. devin_metrics-0.2.0/tests/test_render.py +78 -0
  34. devin_metrics-0.2.0/tests/test_smoke.py +4 -0
  35. devin_metrics-0.2.0/tests/test_watch.py +35 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Icaro0310
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: devin-metrics
3
+ Version: 0.2.0
4
+ Summary: Activity, context and session observability across Devin sessions — local-only, no telemetry.
5
+ Author: Icaro0310
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: devin-internals-spec<0.4.0,>=0.3.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == "dev"
13
+ Dynamic: license-file
14
+
15
+ <div align="center">
16
+
17
+ <img src="assets/banner.svg" alt="devin-metrics" width="100%"/>
18
+
19
+ <a href="https://github.com/Icaro0310/devin-metrics/actions/workflows/ci.yml"><img src="https://github.com/Icaro0310/devin-metrics/actions/workflows/ci.yml/badge.svg" alt="ci"/></a>
20
+
21
+
22
+ <a href="https://scorecard.dev/viewer/?uri=github.com/Icaro0310/devin-metrics"><img src="https://api.scorecard.dev/projects/github.com/Icaro0310/devin-metrics/badge" alt="OpenSSF Scorecard"/></a>
23
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT"/></a>
24
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python 3.10+"/></a>
25
+ <a href="https://github.com/Icaro0310/devin-metrics"><img src="https://img.shields.io/github/stars/Icaro0310/devin-metrics" alt="GitHub stars"/></a>
26
+ <a href="https://github.com/Icaro0310/devin-metrics/commits/main"><img src="https://img.shields.io/github/last-commit/Icaro0310/devin-metrics" alt="Last commit"/></a>
27
+ <a href="https://github.com/Icaro0310/awesome-devin"><img src="https://img.shields.io/badge/part%20of-devin--*-ecosystem-7c3aed" alt="devin-* ecosystem"/></a>
28
+ <a href="https://github.com/Icaro0310/devin-metrics/issues"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"/></a>
29
+ </div>
30
+
31
+ # devin-metrics
32
+
33
+ > **Unofficial community project.** Not affiliated with, endorsed by, or
34
+ > sponsored by Cognition AI. "Devin" is a trademark of Cognition AI.
35
+
36
+ **[Linux](README.linux.md)** · **[Personal Windows](README.windows.md)** · **[Corporate Windows](README.corporate-windows.md)**
37
+
38
+ Part of the [awesome-devin](https://github.com/Icaro0310/awesome-devin) ecosystem: the curated hub for the devin-* tools.
39
+
40
+ Local-only metrics for your Devin usage: sessions per day/week, per-project
41
+ and per-model rollups, context-size peaks, longest sessions, tool-call mix —
42
+ zero telemetry, JSON + markdown output.
43
+
44
+ ## The problem
45
+
46
+ Devin sessions accumulate real activity — context growth, model time, tool
47
+ calls — but there is no way to answer "which project ate my week?" or "how
48
+ big did my sessions get?". The data already exists on disk in `sessions.db`
49
+ and `acp-messages/*.db`; nothing reads it. `devin-metrics` is the missing
50
+ read side.
51
+
52
+ ## Prior art
53
+
54
+ Agent-usage trackers exist for other tools — e.g. `ccusage` for Claude Code
55
+ reads `~/.claude` transcripts and reports cost/token rollups. This project
56
+ adapts the same idea; it does not reinvent it. What is different here is the
57
+ *source*: Devin's stores are private, schema-versioned (17 migrations), and
58
+ undocumented — so the reading layer is delegated to
59
+ [`devin-internals-spec`](https://github.com/Icaro0310/devin-internals-spec)
60
+ which owns parsing + schema detection.
61
+
62
+ ## What makes it Devin-native
63
+
64
+ - **Side-by-side:** generic token trackers cannot open Devin's stores at
65
+ all — the format is unpublished. This tool reads them directly, so
66
+ metrics come from protocol data (`sessions.db`, `acp-messages`), not
67
+ scraped text.
68
+ - **No-Devin:** remove Devin and there is nothing to measure — no store, no
69
+ metrics.
70
+ - **One sentence:** it reads Devin's own databases and tells you what your
71
+ sessions did — locally, with nothing sent anywhere.
72
+
73
+ Per-session `working_directory` gives project attribution for free.
74
+
75
+ ## Install
76
+
77
+ Python ≥ 3.10 and `pipx` are required. **Windows (PowerShell):** install `pipx` with `py -m pip install --user pipx`, run `py -m pipx ensurepath`, then reopen the terminal. **Linux (Debian/Ubuntu):** run `sudo apt install pipx python3-venv` and `pipx ensurepath`; reopen the terminal. Other Linux distributions should install `pipx` using their package manager.
78
+
79
+ This package is not on PyPI yet; install the public GitHub version:
80
+
81
+ ```bash
82
+ pipx install "devin-metrics @ git+https://github.com/Icaro0310/devin-metrics.git"
83
+ ```
84
+
85
+ ## Usage
86
+
87
+ ```bash
88
+ devin-metrics summary # headline numbers + top-5 lists
89
+ devin-metrics projects # per-project session/activity table
90
+ devin-metrics daily --days 14 # activity over time
91
+ devin-metrics dashboard --out usage.html
92
+
93
+ devin-dashboard build --out usage.html # dashboard executable alias
94
+ devin-dashboard data --json # same dashboard source data as JSON
95
+ devin-metrics summary --json # raw JSON for scripting
96
+ ```
97
+
98
+ `devin-dashboard` is an alias shipped by the same package. Its `build` command
99
+ writes a standalone HTML dashboard; `data` prints the normalized stats payload.
100
+
101
+ By default, `sessions.db` is read from the platform data root (`%APPDATA%/devin`
102
+ on Windows, `$XDG_DATA_HOME/devin` on Linux, normally `~/.local/share/devin`).
103
+ ACP logs are read from the separate UI config root (`%APPDATA%/Devin/User`
104
+ on Windows, `$XDG_CONFIG_HOME/Devin/User` on Linux). Override with
105
+ `--data-dir`, `--sessions-db` or `--acp-dir`.
106
+
107
+ ```bash
108
+ devin-metrics summary --sessions-db path/to/sessions.db --acp-dir path/to/acp-messages
109
+ ```
110
+
111
+ A missing `acp-messages` dir degrades gracefully: `cost_usd` shows `-`
112
+ (unknown ≠ zero). Note that per-turn cost is **not persisted** even when
113
+ the dir exists (verified — see Limitations); `context_tokens` is the real
114
+ per-session token signal.
115
+
116
+ ## Works with Devin alone (Devin-only mode)
117
+
118
+ All metrics are computed locally from Devin's own stores and written to a
119
+ local database — zero telemetry, zero network calls. The `devin-dashboard`
120
+ console alias included in this package (it absorbed the old standalone
121
+ dashboard) also renders entirely on your machine.
122
+
123
+ ## Platform support
124
+
125
+ Tested on **Windows and Linux** (`windows-latest` + `ubuntu-latest` in CI).
126
+ The CLI database is auto-detected from `%APPDATA%/devin/cli/sessions.db` on
127
+ Windows and `$XDG_DATA_HOME/devin/cli/sessions.db` on Linux (default
128
+ `~/.local/share/devin/cli/sessions.db`). ACP logs are read from
129
+ `$XDG_CONFIG_HOME/Devin/User/acp-messages` (default
130
+ `~/.config/Devin/User/acp-messages`). Legacy `~/.config/devin` layouts are
131
+ also checked. Override with `--sessions-db` or `--acp-dir`.
132
+
133
+
134
+ The dashboard also charts **peak `num_tokens_preceding` per day** — the only token signal persisted locally (verified: no cost fields are stored). Cost charts show a "no data" note rather than fake zeros.
135
+
136
+
137
+ ### `devin-metrics churn` (needs `devin-graph build`)
138
+
139
+ Rework stats from the knowledge graph: files re-touched by multiple tool calls in the same session, per session and per model. Known noise: pseudo-paths like `/dev/null` and shell builtins can rank high — they are real `file_touched` edges, just not meaningful rework.
140
+
141
+
142
+ `devin-metrics watch` is an **advisory** context guard (ME-2): lists
143
+ sessions/days whose `context_tokens` exceed thresholds
144
+ (`--session-warn`, `--daily-warn`, `--fail` for CI). It never blocks —
145
+ and it watches *context size*, not cost: local stores have no cost data
146
+ (verified, see SCHEMA.md).
147
+
148
+ ## Limitations
149
+
150
+ - **Read-only, no network.** Stores are opened `mode=ro`; nothing is
151
+ written or sent anywhere.
152
+ - **Cost is not persisted locally — verified.** A real install (2026-10)
153
+ confirms acp payloads and `tool_call_state` carry **no** cost/token
154
+ fields; per-turn cost lives only in the live ACP session meta and is
155
+ never written to disk. `cost_usd` therefore shows `-` on real data.
156
+ The one token signal that *does* persist — `num_tokens_preceding` in
157
+ `message_nodes.metadata` — is reported per session as `context_tokens`
158
+ (peak context size). Details: `docs/SCHEMA.md`.
159
+ - **Schema-gated.** `sessions.db` versions outside v15–v17 are refused
160
+ loudly (via `devin-internals-spec`'s detector) rather than misread.
161
+ - Drift-checked — `devin-inspect contract` (from devin-internals-spec)
162
+ validates this install against every known contract boundary.
163
+
164
+ ## Development
165
+
166
+ ```bash
167
+ pip install -e ".[dev]"
168
+ python -m pytest
169
+ ```
170
+
171
+ ## When to use this
172
+
173
+ - You want a local view of Devin activity: sessions per project, model,
174
+ or day, context-size peaks, longest sessions and tool-call mix.
175
+ - You need a scriptable JSON feed of usage stats (`--json` on every command).
176
+ - You want a standalone HTML dashboard of activity (`devin-metrics dashboard`
177
+ or the `devin-dashboard` alias).
178
+ - Telemetry is a hard no — everything is computed and stored locally.
179
+
180
+ ## When NOT to use this
181
+
182
+ - You need to search message content — use `devin-search`; or relationship
183
+ queries across sessions/files/tools — use `devin-graph`.
184
+ - You need live, real-time session monitoring — use `devin-office`.
185
+ - The machine has no Devin CLI/Desktop install — there is nothing to measure.
186
+
187
+ ## FAQ
188
+
189
+ **What is devin-metrics?** A local CLI that reads Devin's own session
190
+ databases and reports local observability metrics: sessions per day/week,
191
+ activity and context-size peaks per project and model, longest sessions, and
192
+ tool-call mix. It also
193
+ ships a `devin-dashboard` alias that writes a standalone HTML dashboard.
194
+
195
+ **How does devin-metrics get cost data?** Honest answer: it mostly
196
+ doesn't — verified on a real install, Devin's local stores persist **no**
197
+ cost or token fields (cost exists only in the live ACP session meta and
198
+ is never written to disk). What it does measure: sessions, messages,
199
+ tool calls, durations, per-project/per-model rollups, and `context_tokens`
200
+ (peak `num_tokens_preceding` — the only token signal that persists). The
201
+ `extract_usage()` adapter remains ready if a future schema starts
202
+ persisting cost.
203
+
204
+ **Does devin-metrics send data anywhere?** No. All metrics are computed
205
+ locally and written to a local database. There are no network calls and no
206
+ telemetry; Devin's own stores are opened `mode=ro` and never written.
207
+
208
+ ## License
209
+
210
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,196 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/banner.svg" alt="devin-metrics" width="100%"/>
4
+
5
+ <a href="https://github.com/Icaro0310/devin-metrics/actions/workflows/ci.yml"><img src="https://github.com/Icaro0310/devin-metrics/actions/workflows/ci.yml/badge.svg" alt="ci"/></a>
6
+
7
+
8
+ <a href="https://scorecard.dev/viewer/?uri=github.com/Icaro0310/devin-metrics"><img src="https://api.scorecard.dev/projects/github.com/Icaro0310/devin-metrics/badge" alt="OpenSSF Scorecard"/></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT"/></a>
10
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python 3.10+"/></a>
11
+ <a href="https://github.com/Icaro0310/devin-metrics"><img src="https://img.shields.io/github/stars/Icaro0310/devin-metrics" alt="GitHub stars"/></a>
12
+ <a href="https://github.com/Icaro0310/devin-metrics/commits/main"><img src="https://img.shields.io/github/last-commit/Icaro0310/devin-metrics" alt="Last commit"/></a>
13
+ <a href="https://github.com/Icaro0310/awesome-devin"><img src="https://img.shields.io/badge/part%20of-devin--*-ecosystem-7c3aed" alt="devin-* ecosystem"/></a>
14
+ <a href="https://github.com/Icaro0310/devin-metrics/issues"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"/></a>
15
+ </div>
16
+
17
+ # devin-metrics
18
+
19
+ > **Unofficial community project.** Not affiliated with, endorsed by, or
20
+ > sponsored by Cognition AI. "Devin" is a trademark of Cognition AI.
21
+
22
+ **[Linux](README.linux.md)** · **[Personal Windows](README.windows.md)** · **[Corporate Windows](README.corporate-windows.md)**
23
+
24
+ Part of the [awesome-devin](https://github.com/Icaro0310/awesome-devin) ecosystem: the curated hub for the devin-* tools.
25
+
26
+ Local-only metrics for your Devin usage: sessions per day/week, per-project
27
+ and per-model rollups, context-size peaks, longest sessions, tool-call mix —
28
+ zero telemetry, JSON + markdown output.
29
+
30
+ ## The problem
31
+
32
+ Devin sessions accumulate real activity — context growth, model time, tool
33
+ calls — but there is no way to answer "which project ate my week?" or "how
34
+ big did my sessions get?". The data already exists on disk in `sessions.db`
35
+ and `acp-messages/*.db`; nothing reads it. `devin-metrics` is the missing
36
+ read side.
37
+
38
+ ## Prior art
39
+
40
+ Agent-usage trackers exist for other tools — e.g. `ccusage` for Claude Code
41
+ reads `~/.claude` transcripts and reports cost/token rollups. This project
42
+ adapts the same idea; it does not reinvent it. What is different here is the
43
+ *source*: Devin's stores are private, schema-versioned (17 migrations), and
44
+ undocumented — so the reading layer is delegated to
45
+ [`devin-internals-spec`](https://github.com/Icaro0310/devin-internals-spec)
46
+ which owns parsing + schema detection.
47
+
48
+ ## What makes it Devin-native
49
+
50
+ - **Side-by-side:** generic token trackers cannot open Devin's stores at
51
+ all — the format is unpublished. This tool reads them directly, so
52
+ metrics come from protocol data (`sessions.db`, `acp-messages`), not
53
+ scraped text.
54
+ - **No-Devin:** remove Devin and there is nothing to measure — no store, no
55
+ metrics.
56
+ - **One sentence:** it reads Devin's own databases and tells you what your
57
+ sessions did — locally, with nothing sent anywhere.
58
+
59
+ Per-session `working_directory` gives project attribution for free.
60
+
61
+ ## Install
62
+
63
+ Python ≥ 3.10 and `pipx` are required. **Windows (PowerShell):** install `pipx` with `py -m pip install --user pipx`, run `py -m pipx ensurepath`, then reopen the terminal. **Linux (Debian/Ubuntu):** run `sudo apt install pipx python3-venv` and `pipx ensurepath`; reopen the terminal. Other Linux distributions should install `pipx` using their package manager.
64
+
65
+ This package is not on PyPI yet; install the public GitHub version:
66
+
67
+ ```bash
68
+ pipx install "devin-metrics @ git+https://github.com/Icaro0310/devin-metrics.git"
69
+ ```
70
+
71
+ ## Usage
72
+
73
+ ```bash
74
+ devin-metrics summary # headline numbers + top-5 lists
75
+ devin-metrics projects # per-project session/activity table
76
+ devin-metrics daily --days 14 # activity over time
77
+ devin-metrics dashboard --out usage.html
78
+
79
+ devin-dashboard build --out usage.html # dashboard executable alias
80
+ devin-dashboard data --json # same dashboard source data as JSON
81
+ devin-metrics summary --json # raw JSON for scripting
82
+ ```
83
+
84
+ `devin-dashboard` is an alias shipped by the same package. Its `build` command
85
+ writes a standalone HTML dashboard; `data` prints the normalized stats payload.
86
+
87
+ By default, `sessions.db` is read from the platform data root (`%APPDATA%/devin`
88
+ on Windows, `$XDG_DATA_HOME/devin` on Linux, normally `~/.local/share/devin`).
89
+ ACP logs are read from the separate UI config root (`%APPDATA%/Devin/User`
90
+ on Windows, `$XDG_CONFIG_HOME/Devin/User` on Linux). Override with
91
+ `--data-dir`, `--sessions-db` or `--acp-dir`.
92
+
93
+ ```bash
94
+ devin-metrics summary --sessions-db path/to/sessions.db --acp-dir path/to/acp-messages
95
+ ```
96
+
97
+ A missing `acp-messages` dir degrades gracefully: `cost_usd` shows `-`
98
+ (unknown ≠ zero). Note that per-turn cost is **not persisted** even when
99
+ the dir exists (verified — see Limitations); `context_tokens` is the real
100
+ per-session token signal.
101
+
102
+ ## Works with Devin alone (Devin-only mode)
103
+
104
+ All metrics are computed locally from Devin's own stores and written to a
105
+ local database — zero telemetry, zero network calls. The `devin-dashboard`
106
+ console alias included in this package (it absorbed the old standalone
107
+ dashboard) also renders entirely on your machine.
108
+
109
+ ## Platform support
110
+
111
+ Tested on **Windows and Linux** (`windows-latest` + `ubuntu-latest` in CI).
112
+ The CLI database is auto-detected from `%APPDATA%/devin/cli/sessions.db` on
113
+ Windows and `$XDG_DATA_HOME/devin/cli/sessions.db` on Linux (default
114
+ `~/.local/share/devin/cli/sessions.db`). ACP logs are read from
115
+ `$XDG_CONFIG_HOME/Devin/User/acp-messages` (default
116
+ `~/.config/Devin/User/acp-messages`). Legacy `~/.config/devin` layouts are
117
+ also checked. Override with `--sessions-db` or `--acp-dir`.
118
+
119
+
120
+ The dashboard also charts **peak `num_tokens_preceding` per day** — the only token signal persisted locally (verified: no cost fields are stored). Cost charts show a "no data" note rather than fake zeros.
121
+
122
+
123
+ ### `devin-metrics churn` (needs `devin-graph build`)
124
+
125
+ Rework stats from the knowledge graph: files re-touched by multiple tool calls in the same session, per session and per model. Known noise: pseudo-paths like `/dev/null` and shell builtins can rank high — they are real `file_touched` edges, just not meaningful rework.
126
+
127
+
128
+ `devin-metrics watch` is an **advisory** context guard (ME-2): lists
129
+ sessions/days whose `context_tokens` exceed thresholds
130
+ (`--session-warn`, `--daily-warn`, `--fail` for CI). It never blocks —
131
+ and it watches *context size*, not cost: local stores have no cost data
132
+ (verified, see SCHEMA.md).
133
+
134
+ ## Limitations
135
+
136
+ - **Read-only, no network.** Stores are opened `mode=ro`; nothing is
137
+ written or sent anywhere.
138
+ - **Cost is not persisted locally — verified.** A real install (2026-10)
139
+ confirms acp payloads and `tool_call_state` carry **no** cost/token
140
+ fields; per-turn cost lives only in the live ACP session meta and is
141
+ never written to disk. `cost_usd` therefore shows `-` on real data.
142
+ The one token signal that *does* persist — `num_tokens_preceding` in
143
+ `message_nodes.metadata` — is reported per session as `context_tokens`
144
+ (peak context size). Details: `docs/SCHEMA.md`.
145
+ - **Schema-gated.** `sessions.db` versions outside v15–v17 are refused
146
+ loudly (via `devin-internals-spec`'s detector) rather than misread.
147
+ - Drift-checked — `devin-inspect contract` (from devin-internals-spec)
148
+ validates this install against every known contract boundary.
149
+
150
+ ## Development
151
+
152
+ ```bash
153
+ pip install -e ".[dev]"
154
+ python -m pytest
155
+ ```
156
+
157
+ ## When to use this
158
+
159
+ - You want a local view of Devin activity: sessions per project, model,
160
+ or day, context-size peaks, longest sessions and tool-call mix.
161
+ - You need a scriptable JSON feed of usage stats (`--json` on every command).
162
+ - You want a standalone HTML dashboard of activity (`devin-metrics dashboard`
163
+ or the `devin-dashboard` alias).
164
+ - Telemetry is a hard no — everything is computed and stored locally.
165
+
166
+ ## When NOT to use this
167
+
168
+ - You need to search message content — use `devin-search`; or relationship
169
+ queries across sessions/files/tools — use `devin-graph`.
170
+ - You need live, real-time session monitoring — use `devin-office`.
171
+ - The machine has no Devin CLI/Desktop install — there is nothing to measure.
172
+
173
+ ## FAQ
174
+
175
+ **What is devin-metrics?** A local CLI that reads Devin's own session
176
+ databases and reports local observability metrics: sessions per day/week,
177
+ activity and context-size peaks per project and model, longest sessions, and
178
+ tool-call mix. It also
179
+ ships a `devin-dashboard` alias that writes a standalone HTML dashboard.
180
+
181
+ **How does devin-metrics get cost data?** Honest answer: it mostly
182
+ doesn't — verified on a real install, Devin's local stores persist **no**
183
+ cost or token fields (cost exists only in the live ACP session meta and
184
+ is never written to disk). What it does measure: sessions, messages,
185
+ tool calls, durations, per-project/per-model rollups, and `context_tokens`
186
+ (peak `num_tokens_preceding` — the only token signal that persists). The
187
+ `extract_usage()` adapter remains ready if a future schema starts
188
+ persisting cost.
189
+
190
+ **Does devin-metrics send data anywhere?** No. All metrics are computed
191
+ locally and written to a local database. There are no network calls and no
192
+ telemetry; Devin's own stores are opened `mode=ro` and never written.
193
+
194
+ ## License
195
+
196
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "devin-metrics"
7
+ version = "0.2.0"
8
+ description = "Activity, context and session observability across Devin sessions — local-only, no telemetry."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Icaro0310" }]
13
+ dependencies = [
14
+ "devin-internals-spec>=0.3.0,<0.4.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=8"]
19
+
20
+ [project.scripts]
21
+ devin-metrics = "devin_metrics.cli:main"
22
+ devin-dashboard = "devin_metrics.dashboard.cli:main"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
29
+ pythonpath = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,199 @@
1
+ """Rollups over a :class:`MetricsSnapshot` — pure functions, JSON-ready dicts.
2
+
3
+ Attribution rules:
4
+
5
+ - Sessions group by ``working_directory`` (the project) — free with Devin's
6
+ session store.
7
+ - Day buckets use the session's ``created_at`` in UTC.
8
+ - ``cost_usd_total`` counts **all** acp usage rows (including orphan dbs with
9
+ no session row); ``cost_usd_by_sessions`` counts only matched sessions.
10
+ - ``cost_usd is None`` means *unknown* (no acp data), never zero.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from datetime import datetime, timezone
16
+ from typing import Any, Iterable
17
+
18
+ from devin_metrics.collect import MetricsSnapshot, SessionMetrics, UsageRecord
19
+
20
+
21
+ def _day(ms: int) -> str:
22
+ return datetime.fromtimestamp(ms / 1000, tz=timezone.utc).date().isoformat()
23
+
24
+
25
+ def session_to_dict(s: SessionMetrics) -> dict[str, Any]:
26
+ return {
27
+ "id": s.id,
28
+ "title": s.title,
29
+ "project": s.working_directory,
30
+ "model": s.model,
31
+ "created": _day(s.created_at),
32
+ "created_at": s.created_at,
33
+ "duration_ms": s.duration_ms,
34
+ "messages": s.n_messages,
35
+ "tool_calls": s.n_tool_calls,
36
+ "cost_usd": s.cost_usd,
37
+ "input_tokens": s.input_tokens,
38
+ "output_tokens": s.output_tokens,
39
+ "context_tokens": s.context_tokens,
40
+ }
41
+
42
+
43
+ def _sum_or_none(values: Iterable[float | int | None]) -> float | int | None:
44
+ known = [v for v in values if v is not None]
45
+ return sum(known) if known else None
46
+
47
+
48
+ def _sum(values: Iterable[float | int]) -> float | int:
49
+ return sum(values)
50
+
51
+
52
+ # -- summaries ---------------------------------------------------------------
53
+
54
+
55
+ def summarize(snap: MetricsSnapshot) -> dict[str, Any]:
56
+ sessions = list(snap.sessions)
57
+ n = len(sessions)
58
+ with_cost = [s for s in sessions if s.cost_usd is not None]
59
+ return {
60
+ "sessions": n,
61
+ "sessions_with_cost": len(with_cost),
62
+ "hidden_sessions": sum(1 for s in sessions if s.hidden),
63
+ "first_seen": _day(min(s.created_at for s in sessions)) if n else None,
64
+ "last_seen": _day(max(s.last_activity_at for s in sessions)) if n else None,
65
+ "messages": _sum(s.n_messages for s in sessions),
66
+ "tool_calls": _sum(s.n_tool_calls for s in sessions),
67
+ "duration_ms_total": _sum(s.duration_ms for s in sessions),
68
+ "cost_usd_total": _sum_or_none(u.cost_usd for u in snap.usage),
69
+ "cost_usd_by_sessions": _sum_or_none(s.cost_usd for s in sessions),
70
+ "input_tokens_total": _sum_or_none(u.input_tokens for u in snap.usage),
71
+ "output_tokens_total": _sum_or_none(u.output_tokens for u in snap.usage),
72
+ "context_tokens_peak": max(
73
+ (s.context_tokens for s in sessions
74
+ if s.context_tokens is not None),
75
+ default=None,
76
+ ),
77
+ "sessions_with_context": sum(
78
+ 1 for s in sessions if s.context_tokens is not None
79
+ ),
80
+ "avg_duration_ms": (_sum(s.duration_ms for s in sessions) / n) if n else None,
81
+ "avg_messages": (_sum(s.n_messages for s in sessions) / n) if n else None,
82
+ "avg_cost_usd": (
83
+ _sum(s.cost_usd for s in with_cost) / len(with_cost) # type: ignore[arg-type]
84
+ if with_cost
85
+ else None
86
+ ),
87
+ "acp_available": snap.acp_available,
88
+ "acp_db_files": snap.acp_db_files,
89
+ "acp_db_errors": snap.acp_db_errors,
90
+ "matched_sessions": snap.matched_sessions,
91
+ "orphan_dbs": snap.orphan_dbs,
92
+ "top_longest": top_sessions(snap, n=5, key="duration_ms"),
93
+ "top_costliest": top_sessions(snap, n=5, key="cost_usd"),
94
+ "models": by_model(snap),
95
+ }
96
+
97
+
98
+ def by_project(snap: MetricsSnapshot) -> list[dict[str, Any]]:
99
+ groups: dict[str, list[SessionMetrics]] = {}
100
+ for s in snap.sessions:
101
+ groups.setdefault(s.working_directory, []).append(s)
102
+ rows = [
103
+ {
104
+ "project": wd,
105
+ "sessions": len(members),
106
+ "messages": _sum(s.n_messages for s in members),
107
+ "tool_calls": _sum(s.n_tool_calls for s in members),
108
+ "duration_ms": _sum(s.duration_ms for s in members),
109
+ "cost_usd": _sum_or_none(s.cost_usd for s in members),
110
+ "input_tokens": _sum_or_none(s.input_tokens for s in members),
111
+ "output_tokens": _sum_or_none(s.output_tokens for s in members),
112
+ }
113
+ for wd, members in groups.items()
114
+ ]
115
+ rows.sort(
116
+ key=lambda r: (r["cost_usd"] is not None, r["cost_usd"] or 0, r["sessions"]),
117
+ reverse=True,
118
+ )
119
+ return rows
120
+
121
+
122
+ def by_model(snap: MetricsSnapshot) -> list[dict[str, Any]]:
123
+ """Per-model stats merging session-declared models and acp usage rows."""
124
+ rows: dict[str, dict[str, Any]] = {}
125
+
126
+ def _row(model: str | None) -> dict[str, Any]:
127
+ name = model or "(unknown)"
128
+ return rows.setdefault(
129
+ name,
130
+ {
131
+ "model": name,
132
+ "sessions": 0,
133
+ "usage_records": 0,
134
+ "cost_usd": None,
135
+ "input_tokens": None,
136
+ "output_tokens": None,
137
+ },
138
+ )
139
+
140
+ for s in snap.sessions:
141
+ _row(s.model)["sessions"] += 1
142
+ for u in snap.usage:
143
+ r = _row(u.model)
144
+ r["usage_records"] += 1
145
+ if u.cost_usd is not None:
146
+ r["cost_usd"] = (r["cost_usd"] or 0.0) + u.cost_usd
147
+ if u.input_tokens is not None:
148
+ r["input_tokens"] = (r["input_tokens"] or 0) + u.input_tokens
149
+ if u.output_tokens is not None:
150
+ r["output_tokens"] = (r["output_tokens"] or 0) + u.output_tokens
151
+ out = list(rows.values())
152
+ out.sort(
153
+ key=lambda r: (r["cost_usd"] is not None, r["cost_usd"] or 0, r["sessions"]),
154
+ reverse=True,
155
+ )
156
+ return out
157
+
158
+
159
+ def by_day(snap: MetricsSnapshot, days: int | None = None) -> list[dict[str, Any]]:
160
+ """Sessions bucketed by UTC creation date.
161
+
162
+ ``days=N`` keeps the N most recent *activity* days (anchored on the latest
163
+ day in the data, not on wall-clock now — deterministic for fixtures and
164
+ for inspecting old installs alike).
165
+ """
166
+ groups: dict[str, list[SessionMetrics]] = {}
167
+ for s in snap.sessions:
168
+ groups.setdefault(_day(s.created_at), []).append(s)
169
+ dates = sorted(groups)
170
+ if days is not None:
171
+ dates = dates[-days:] if days > 0 else []
172
+ return [
173
+ {
174
+ "date": d,
175
+ "sessions": len(groups[d]),
176
+ "messages": _sum(s.n_messages for s in groups[d]),
177
+ "tool_calls": _sum(s.n_tool_calls for s in groups[d]),
178
+ "duration_ms": _sum(s.duration_ms for s in groups[d]),
179
+ "cost_usd": _sum_or_none(s.cost_usd for s in groups[d]),
180
+ }
181
+ for d in dates
182
+ ]
183
+
184
+
185
+ def top_sessions(
186
+ snap: MetricsSnapshot, n: int = 5, key: str = "duration_ms"
187
+ ) -> list[dict[str, Any]]:
188
+ """Top-N sessions. ``key``: ``duration_ms`` or ``cost_usd``.
189
+
190
+ For ``cost_usd`` sessions with unknown cost are excluded entirely.
191
+ """
192
+ if key == "cost_usd":
193
+ pool = [s for s in snap.sessions if s.cost_usd is not None]
194
+ ordered = sorted(pool, key=lambda s: s.cost_usd or 0, reverse=True)
195
+ elif key == "duration_ms":
196
+ ordered = sorted(snap.sessions, key=lambda s: s.duration_ms, reverse=True)
197
+ else:
198
+ raise ValueError(f"top_sessions: unknown key {key!r}")
199
+ return [session_to_dict(s) for s in ordered[:n]]