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.
- devin_metrics-0.2.0/LICENSE +21 -0
- devin_metrics-0.2.0/PKG-INFO +210 -0
- devin_metrics-0.2.0/README.md +196 -0
- devin_metrics-0.2.0/pyproject.toml +29 -0
- devin_metrics-0.2.0/setup.cfg +4 -0
- devin_metrics-0.2.0/src/devin_metrics/__init__.py +1 -0
- devin_metrics-0.2.0/src/devin_metrics/aggregate.py +199 -0
- devin_metrics-0.2.0/src/devin_metrics/churn.py +141 -0
- devin_metrics-0.2.0/src/devin_metrics/cli.py +246 -0
- devin_metrics-0.2.0/src/devin_metrics/collect.py +240 -0
- devin_metrics-0.2.0/src/devin_metrics/dashboard/__init__.py +1 -0
- devin_metrics-0.2.0/src/devin_metrics/dashboard/cli.py +137 -0
- devin_metrics-0.2.0/src/devin_metrics/dashboard/collect.py +415 -0
- devin_metrics-0.2.0/src/devin_metrics/dashboard/render.py +287 -0
- devin_metrics-0.2.0/src/devin_metrics/paths.py +81 -0
- devin_metrics-0.2.0/src/devin_metrics/render.py +170 -0
- devin_metrics-0.2.0/src/devin_metrics.egg-info/PKG-INFO +210 -0
- devin_metrics-0.2.0/src/devin_metrics.egg-info/SOURCES.txt +33 -0
- devin_metrics-0.2.0/src/devin_metrics.egg-info/dependency_links.txt +1 -0
- devin_metrics-0.2.0/src/devin_metrics.egg-info/entry_points.txt +3 -0
- devin_metrics-0.2.0/src/devin_metrics.egg-info/requires.txt +4 -0
- devin_metrics-0.2.0/src/devin_metrics.egg-info/top_level.txt +1 -0
- devin_metrics-0.2.0/tests/test_aggregate.py +161 -0
- devin_metrics-0.2.0/tests/test_churn.py +73 -0
- devin_metrics-0.2.0/tests/test_cli.py +84 -0
- devin_metrics-0.2.0/tests/test_collect.py +81 -0
- devin_metrics-0.2.0/tests/test_dashboard_cli.py +76 -0
- devin_metrics-0.2.0/tests/test_dashboard_collect.py +115 -0
- devin_metrics-0.2.0/tests/test_dashboard_render.py +73 -0
- devin_metrics-0.2.0/tests/test_dashboard_smoke.py +4 -0
- devin_metrics-0.2.0/tests/test_offline_core.py +76 -0
- devin_metrics-0.2.0/tests/test_paths.py +29 -0
- devin_metrics-0.2.0/tests/test_render.py +78 -0
- devin_metrics-0.2.0/tests/test_smoke.py +4 -0
- 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 @@
|
|
|
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]]
|