Data analytics & DWH integration¶
Codefang produces richly structured JSON output designed for loading into columnar data warehouses (ClickHouse, Greenplum, BigQuery, Snowflake) and building BI dashboards. This guide covers the optimal pipeline from repository analysis to production dashboards.
Quick start¶
Analyze a repository and produce DWH-ready output:
# JSON for small-to-medium repos (< 5K files, < 10K commits)
codefang run --format json --per-file /path/to/repo > report.json
# NDJSON for large repos (one line per analyzer)
codefang run --format ndjson /path/to/repo > report.ndjson
# Limit history depth for faster iteration
codefang run --format json --per-file --limit 5000 /path/to/repo > report.json
Output format selection¶
| Repo Size | Recommended Format | Reason |
|---|---|---|
| < 1K files | json | Small file, easy to inspect |
| 1K-10K files | json | Manageable (< 500MB typically) |
| 10K-50K files | ndjson | JSON gets multi-GB; NDJSON streams |
| 50K+ files | ndjson + --limit | Bound history for practical runtimes |
JSON format¶
Produces a single JSON object with versioned envelope:
{
"version": "codefang.run.v1",
"metadata": {
"repo_name": "myproject",
"analyzed_at": "2026-04-08T10:00:00Z",
"codefang_version": "0.1.0"
},
"analyzers": [
{
"id": "static/complexity",
"mode": "static",
"schema": { ... },
"report": { ... }
}
]
}
NDJSON format¶
One JSON line per analyzer. First line is metadata:
{"version":"codefang.run.v1","metadata":{"repo_name":"myproject",...}}
{"id":"static/complexity","mode":"static","report":{...}}
{"id":"history/sentiment","mode":"history","report":{...}}
Process with standard tools:
# Extract one analyzer
grep '"static/complexity"' report.ndjson | jq .report.aggregate
# Count analyzers
wc -l report.ndjson
# Stream into ClickHouse
cat report.ndjson | clickhouse-client --query "INSERT INTO codefang_raw FORMAT JSONEachRow"
Bounding a history run¶
--memory-budget (default: half the RAM, at most 2 GiB) bounds what the history walks hold in flight: each walk plans its chunk size and worker count from its share, and -v prints the plan. The output never depends on the budget. The analyzers' own aggregates still grow with the commit window, so on a very large repository also bound the window:
| Flag | Effect |
|---|---|
--limit N | Only the N newest commits |
--since <time or revision> | Only commits authored at/after the cutoff (4380h, 2025-01-01, v1.2.0) |
--first-parent | Skip the side branches of merges |
--max-changes-per-commit N | Drop commits whose tree diff exceeds N changes (default 10000) — vendoring dumps, mass renames |
--languages go,python | Only changes in these languages reach the analyzers |
--head | The single HEAD commit |
Measured on the hercules repository, codefang run -a 'history/*' --limit 500 peaks at 752 MB resident memory and finishes in 4.7 s wall time.
# The newest 5000 commits of a kubernetes-sized repository
codefang run --format ndjson --limit 5000 ~/sources/kubernetes > report.ndjson
Commit limiting¶
Use --limit N to analyze only the most recent N commits. This is useful for:
- Fast iteration: Test your ETL pipeline on a subset before running full history
- Incremental analysis: Analyze only recent changes for daily dashboards
- Memory control: Fewer commits = less memory, faster processing
# Last 1000 commits
codefang run --format json --per-file --limit 1000 /repo > recent.json
# Last 10000 commits
codefang run --format json --per-file --limit 10000 /repo > report.json
# Full history
codefang run --format json --per-file /repo > full.json
Key fields for analytics¶
Every function-level record includes fields designed for DWH joins and aggregation:
| Field | Present On | Type | Example | Use Case |
|---|---|---|---|---|
source_file | All function records | string | "pkg/api/server.go" | Join to file-level data |
language | All function records | string | "go" | Group by language |
directory | All function records | string | "pkg/api" | Group by package/module |
start_time | All time-series ticks | RFC 3339 | "2024-01-15T10:30:00Z" | Time-axis labels |
end_time | All time-series ticks | RFC 3339 | "2024-01-16T08:45:00Z" | Tick duration |
name | Developer records | string | "alice" | Developer dimension |
email | Developer records | string | "alice@example.com" | Developer identity |
dev_id | Activity, contributors | int | 42 | Foreign key to developers |
Schema manifest¶
Every analyzer section includes a schema field describing its output:
{
"schema": {
"function_complexity": {
"type": "list",
"grain": "function",
"description": "Per-function cyclomatic and cognitive complexity"
},
"aggregate": {
"type": "aggregate",
"description": "Summary statistics"
}
}
}
Field types: list, aggregate, time_series, risk, scalar
Grain values: function, file, tick, pair, developer, node, comment, import
Use the schema to auto-generate ETL mappings:
# Python: extract schema for table generation
import json
with open('report.json') as f:
data = json.load(f)
for analyzer in data['analyzers']:
schema = analyzer.get('schema', {})
for field, meta in schema.items():
if meta['type'] == 'list':
print(f"CREATE TABLE {analyzer['id'].replace('/', '_')}_{field} ...")
Star schema design¶
Dimensions¶
-- dim_repository
CREATE TABLE dim_repository (
repo_id UInt64,
repo_name String,
repo_path String,
analyzed_at DateTime,
version String
) ENGINE = MergeTree() ORDER BY repo_id;
-- dim_file (extract from source_file + directory + language)
CREATE TABLE dim_file (
file_id UInt64,
repo_id UInt64,
source_file String,
directory String,
language LowCardinality(String)
) ENGINE = MergeTree() ORDER BY (repo_id, source_file);
-- dim_developer
CREATE TABLE dim_developer (
dev_id UInt32,
repo_id UInt64,
name String,
email String
) ENGINE = MergeTree() ORDER BY (repo_id, dev_id);
-- dim_tick
CREATE TABLE dim_tick (
tick_id UInt32,
repo_id UInt64,
tick UInt32,
start_time DateTime,
end_time DateTime
) ENGINE = MergeTree() ORDER BY (repo_id, tick);
Fact tables¶
-- Static analysis facts (per-function grain)
CREATE TABLE fact_function_complexity (
repo_id UInt64,
source_file String,
directory LowCardinality(String),
language LowCardinality(String),
name String,
cyclomatic_complexity UInt32,
cognitive_complexity UInt32,
nesting_depth UInt8,
lines_of_code UInt32,
complexity_density Float64,
risk_level LowCardinality(String)
) ENGINE = MergeTree()
ORDER BY (repo_id, directory, source_file, name);
-- Time-series facts (per-tick grain)
CREATE TABLE fact_tick_sentiment (
repo_id UInt64,
tick UInt32,
start_time DateTime,
end_time DateTime,
sentiment Float32,
classification LowCardinality(String),
comment_count UInt32,
commit_count UInt32
) ENGINE = MergeTree()
ORDER BY (repo_id, tick);
-- Developer activity (per-tick-per-developer grain)
CREATE TABLE fact_developer_activity (
repo_id UInt64,
tick UInt32,
dev_id UInt32,
commits UInt32
) ENGINE = MergeTree()
ORDER BY (repo_id, tick, dev_id);
-- File coupling (per-pair grain)
CREATE TABLE fact_file_coupling (
repo_id UInt64,
file1 String,
file2 String,
co_changes UInt32,
coupling_strength Float64
) ENGINE = MergeTree()
ORDER BY (repo_id, file1, file2);
ETL pipeline¶
Python (with dbt or standalone)¶
import json
with open('report.json') as f:
data = json.load(f)
# Extract metadata
meta = data['metadata']
repo_id = hash(meta['repo_path']) # or use a sequence
# Extract analyzers by ID
analyzers = {a['id']: a['report'] for a in data['analyzers']}
# Load function complexity
functions = analyzers['static/complexity']['function_complexity']
# Each record already has: name, source_file, language, directory,
# cyclomatic_complexity, cognitive_complexity, etc.
# Load time-series with timestamps
sentiment_ts = analyzers['history/sentiment']['time_series']
# Each tick has: tick, start_time, end_time, sentiment, classification, ...
# Load developers
developers = analyzers['history/devs']['developers']
# Each has: id, name, email, commits, lines_added, languages (array), ...
# Load file coupling (can be millions of rows)
coupling = analyzers['history/couples']['file_coupling']
# Each has: file1, file2, co_changes, coupling_strength
ClickHouse direct load¶
# Extract function complexity from NDJSON
grep '"static/complexity"' report.ndjson \
| jq -c '.report.function_complexity[]' \
| clickhouse-client --query "INSERT INTO fact_function_complexity FORMAT JSONEachRow"
# Extract sentiment time-series
grep '"history/sentiment"' report.ndjson \
| jq -c '.report.time_series[]' \
| clickhouse-client --query "INSERT INTO fact_tick_sentiment FORMAT JSONEachRow"
Recommended analyzer selection¶
Not all 17 analyzers are needed for every use case. Select based on your dashboard needs:
Code quality dashboard¶
codefang run \
-a static/complexity,static/halstead,static/cohesion,static/comments \
-a history/quality \
--format json --per-file /repo
Produces: Function-level metrics, quality trend over time. Row count: ~200K functions + ~4K tick entries for a medium repo.
Developer analytics dashboard¶
Produces: Developer profiles, coupling networks, sentiment trends. Row count: ~500 developers + ~5K coupling pairs + ~4K ticks.
File health dashboard¶
codefang run \
-a static/complexity,static/clones \
-a history/file-history,history/couples \
--format json --per-file /repo
Produces: Per-file complexity, churn hotspots, coupling networks. Row count: ~30K files + ~100K coupling pairs.
Full analysis (everything)¶
Produces: All 17 analyzers. Use NDJSON for large repos.
Performance tuning¶
Static analysis¶
The static phase is one walk of the working tree shared by every static/* analyzer; each admitted file is read once. Narrow it with --languages and --extra-excluded-prefixes; vendored and generated files are excluded by default (--include-vendored / --include-generated re-include them).
History analysis¶
The history phase has no tuning flags beyond the commit window (see Bounding a history run):
| Parameter | Flag | Default | Effect |
|---|---|---|---|
| Commit limit | --limit | 0 (all) | Analyze at most N commits — the N newest |
| First parent | --first-parent | false | Skip merge side branches |
| Since | --since | none | Time- or revision-based filtering |
| Change cap | --max-changes-per-commit | 0 (= 10000) | Drop oversized commits |
# Analyze only the last 6 months (4380 h), first-parent only
codefang run --since 4380h --first-parent --format json /repo
# ... or since a release tag, capped at the 500 newest commits in the window
codefang run --since v1.28.0 --limit 500 --format json /repo
Duration units are h/m/s only — there is no d/w, and m is minutes, so --since 6m means the last six minutes, not six months.
--since with inactive repos
If no commits fall within the --since window, history analyzers produce empty results (zero ticks, zero developers) and the run still exits 0. Static analyzers still run normally since they analyze the current file tree, not commit history.
Recurring loads¶
There is no incremental cache and no checkpoint: every run analyzes the whole requested window. For a recurring (for example daily) load, bound each run to the commits that are new since the previous one with --since, and load the static snapshot separately:
#!/bin/bash
REPO=/opt/repos/kubernetes
OUTPUT_DIR=/var/lib/codefang/output
DAY=$(date +%Y%m%d)
# Pull latest
cd "$REPO" && git pull --ff-only
# Static snapshot of the working tree
codefang run -a 'static/*' --format json --per-file \
-o "$OUTPUT_DIR/static-$DAY.json" "$REPO"
# History of the last 24 hours, one line per commit and analyzer
codefang run -a 'history/*' --format ndjson --since 24h \
-o "$OUTPUT_DIR/history-$DAY.ndjson" "$REPO"
# Load into ClickHouse
clickhouse-client --query "INSERT INTO codefang_raw FORMAT JSONEachRow" \
< "$OUTPUT_DIR/history-$DAY.ndjson"
A window-bounded history run starts from the tree of the commit just before the window, so a commit's lines added / removed are the lines it changed, as in a full-history run. Whole-history aggregates (totals, bus factor, coupling strength) describe the window only, and burndown counts the lines that existed before the window as its oldest band.
Row count estimates¶
Use these to plan DWH capacity:
| Table | Per 1K Files | Per 10K Commits | Per 50K Files |
|---|---|---|---|
| function_complexity | ~5K | — | ~150K |
| comment_quality | ~17K | — | ~500K |
| file_coupling | — | ~30K | ~4M |
| developer_activity | — | ~3K ticks * devs | ~15K |
| node_coupling | — | ~40K | ~1.5M |
Storage: ~2GB JSON for 50K files + 56K commits (kubernetes scale). Compressed in ClickHouse: ~200MB.
Materialized views¶
Pre-aggregate for common dashboard queries:
-- Complexity by directory (for treemap)
CREATE MATERIALIZED VIEW mv_complexity_by_directory
ENGINE = AggregatingMergeTree() ORDER BY (repo_id, directory)
AS SELECT
repo_id,
directory,
avg(cyclomatic_complexity) AS avg_complexity,
max(cyclomatic_complexity) AS max_complexity,
count() AS function_count,
countIf(risk_level = 'CRITICAL') AS critical_count
FROM fact_function_complexity
GROUP BY repo_id, directory;
-- Sentiment trend (for time-series chart)
CREATE MATERIALIZED VIEW mv_sentiment_weekly
ENGINE = AggregatingMergeTree() ORDER BY (repo_id, week)
AS SELECT
repo_id,
toMonday(start_time) AS week,
avg(sentiment) AS avg_sentiment,
sum(comment_count) AS total_comments
FROM fact_tick_sentiment
GROUP BY repo_id, week;
Troubleshooting¶
High memory on long histories¶
Symptom: the process is killed or swaps during history analysis. Fix: bound the window — --limit, --since, --first-parent, --max-changes-per-commit, --languages.
Empty history analyzers¶
Some analyzers require specific conditions:
| Analyzer | Requirement |
|---|---|
burndown (developer/file survival) | --burndown-people / --burndown-files (config: history.burndown.track_people / track_files) |
history/imports | Only files in a language the UAST parser supports contribute |
history/typos | Only files in a language the UAST parser supports contribute |
Large file coupling tables¶
file_coupling can produce millions of rows for large repos. Filter in your ETL:
Or limit at query time:
SELECT * FROM fact_file_coupling
WHERE coupling_strength > 0.3
ORDER BY coupling_strength DESC
LIMIT 1000;
Missing language/directory on some records¶
The language and directory fields are populated by the UAST parser. If a file's language is not supported by the parser, these fields will be empty. Supported languages include Go, Python, Java, JavaScript, TypeScript, C, C++, Ruby, Rust, and 40+ others.