Skip to content

Add optional stats files to heartbeat export - #1604

Open
wroug wants to merge 8 commits into
hackclub:mainfrom
wroug:add-stats
Open

Add optional stats files to heartbeat export#1604
wroug wants to merge 8 commits into
hackclub:mainfrom
wroug:add-stats

Conversation

@wroug

@wroug wroug commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary of the problem

These changes are made to provide the user an easier way to get the data shown in graphs.

Describe your changes

This pull request adds the ability to include detailed statistics in the heartbeat data export, providing users with additional CSV and JSON files containing breakdowns of their coding activity. The main changes involve updating the export flow to accept a new include_stats parameter, generating various stats files, and enhancing the export job to bundle these files into the exported ZIP archive.

Export Flow Enhancements:

  • The export endpoints in my/heartbeats_controller.rb now accept an include_stats parameter, which is passed to the export job to trigger stats file generation.

Heartbeat Export Job Improvements:

  • The HeartbeatExportJob now accepts and handles the include_stats parameter, generating additional CSV and JSON statistics files (such as project durations, language stats, editor stats, weekly project stats, coding rhythm, etc.) and includes them in the ZIP archive if requested.
  • Several helper methods were added to generate CSV files for different statistics and to collect all stats into the export archive.
  • The job now uses output streams to write files directly into the ZIP archive, improving efficiency and reducing temporary file handling.

Statistics Generation:

  • A new method processed_export_snapshot was added to DashboardData::Snapshots to aggregate all relevant statistics for the export, including project, language, editor, OS, category breakdowns, weekly project stats, and coding rhythm.

Other Minor Changes:

  • Required the csv library in heartbeat_export_job.rb to support CSV generation.
  • Minor formatting improvements for export metadata.
  • A checkbox will be added on the export page to toggle including stats

These changes collectively allow users to optionally receive detailed, structured statistics about their coding activity alongside the standard heartbeat export.

Screenshots / Media

image

@wroug
wroug marked this pull request as draft August 19, 2026 20:10
@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds optional dashboard-statistics files to heartbeat export archives and exposes the option on the imports and exports settings page.

  • Passes the stats option from the Svelte forms through the export controller and background job.
  • Generates grouped CSV and JSON statistics inside the export ZIP.
  • Adds an export-specific dashboard snapshot and regression tests.

Confidence Score: 0/5

This PR is not safe to merge because stats-enabled exports remain unreachable from the UI, fail during weekly snapshot generation and leave one generated CSV formula-active.

The checkbox does not maintain reactive submission state, processed_export_snapshot calls weekly_project_stats with unsupported keywords, weekly ranges still ignore the export dates, weekly project cells bypass CSV sanitisation and the new controller regression test cannot execute its intended path.

Files Needing Attention: app/javascript/pages/Users/Settings/ImportsExports.svelte, app/services/dashboard_data/snapshots.rb, app/jobs/heartbeat_export_job.rb and test/controllers/my/heartbeats_controller_test.rb

Security Review

The weekly project CSV bypasses the formula-prefix sanitisation used by the other grouped CSV files, leaving stored project names formula-active when opened in spreadsheet software.

Important Files Changed

Filename Overview
app/javascript/pages/Users/Settings/ImportsExports.svelte Adds the stats checkbox and hidden fields, but the checkbox state is not reactive so the selection is not reliably submitted.
app/controllers/my/heartbeats_controller.rb Correctly forwards include_stats when the parameter is the literal string "true".
app/jobs/heartbeat_export_job.rb Adds stats files to export archives, but weekly project values bypass the new CSV formula sanitiser.
app/services/dashboard_data/snapshots.rb Adds the export snapshot, but calls weekly_project_stats with unsupported keywords while the underlying range calculation remains fixed to twelve recent weeks.
test/controllers/my/heartbeats_controller_test.rb Adds controller coverage that cannot execute because its user is uninitialised and its parameter values do not enable either boolean.
test/jobs/heartbeat_export_job_test.rb Adds archive-entry coverage for enabled and disabled stats exports, though the enabled path is blocked by the snapshot keyword mismatch.

Sequence Diagram

sequenceDiagram
  participant U as User
  participant S as Settings page
  participant C as Heartbeats controller
  participant J as Heartbeat export job
  participant D as Dashboard snapshots
  participant Z as ZIP archive
  U->>S: Select Include stats and export
  S->>C: POST export parameters
  C->>J: Enqueue export with include_stats
  J->>D: Build export snapshot
  D-->>J: Grouped statistics
  J->>Z: Add heartbeat JSON and stats files
  J-->>U: Email temporary download link
Loading
Prompt To Fix All With AI
### Issue 1
app/javascript/pages/Users/Settings/ImportsExports.svelte:50
**Stats selection stays false**

When a user checks Include stats and submits either export form, `includeStats` is a plain variable rather than Svelte state, so the hidden fields continue submitting `include_stats=false` and the stats option remains unreachable.

```suggestion
  let includeStats = $state(false);
```

### Issue 2
app/services/dashboard_data/snapshots.rb:257
**Unsupported keywords abort stats exports**

When an export requests stats, this call passes `start_date` and `end_date` to `weekly_project_stats`, whose signature accepts only `user` and `scope`. Ruby raises an unknown-keyword `ArgumentError`, so stats generation stops before the files are added to the archive.

### Issue 3
app/jobs/heartbeat_export_job.rb:154
**Weekly projects bypass CSV sanitisation**

When a stored heartbeat project begins with a spreadsheet formula prefix, this writer emits the project directly instead of applying the sanitisation used by `stats_to_csv`, leaving a formula-active cell in `weekly_project_stats.csv` when the export is opened in spreadsheet software.

**How this was verified:** Stored project values retain formula prefixes, flow into weekly project keys and are written here without the adjacent CSV sanitiser.

```suggestion
          safe_project = project.to_s.gsub(/\A([=+\-@\t\r])/, "'\1")
          csv << [week, safe_project, duration]
```

### Issue 4
test/controllers/my/heartbeats_controller_test.rb:76-83
**Stats controller test cannot run**

When this test runs, it dereferences an uninitialised `@user` before posting the request. Even after initialising the user, the submitted values are `"1"` while the controller enables both options only for `"true"`, so the test cannot reach or verify the stats-enabled enqueue path.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (2): Last reviewed commit: "Merge branch 'main' into add-stats" | Re-trigger Greptile

Comment thread app/controllers/my/heartbeats_controller.rb
Comment thread app/services/dashboard_data/snapshots.rb Outdated
Comment thread app/jobs/heartbeat_export_job.rb Outdated
Comment thread app/jobs/heartbeat_export_job.rb Outdated
wroug and others added 3 commits August 20, 2026 00:03
@wroug

wroug commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Current checklist of things until ready for review:

  • Add support for date range exporting
  • Add the client side toggle

#1606 Needs to be merged for this PR to work.

@wroug
wroug marked this pull request as ready for review August 24, 2026 11:21
let remoteApiKey = $state("");
let importOverlay = $state<Partial<HeartbeatImportStatusProps> | null>(null);
let overlayStartTime = $state<number | null>(null);
let includeStats = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stats selection stays false

When a user checks Include stats and submits either export form, includeStats is a plain variable rather than Svelte state, so the hidden fields continue submitting include_stats=false and the stats option remains unreachable.

Suggested change
let includeStats = false;
let includeStats = $state(false);

Context Used: AGENTS.md (source)

Knowledge Base Used: Heartbeat imports and exports

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Users/Settings/ImportsExports.svelte
Line: 50

Comment:
**Stats selection stays false**

When a user checks Include stats and submits either export form, `includeStats` is a plain variable rather than Svelte state, so the hidden fields continue submitting `include_stats=false` and the stats option remains unreachable.

```suggestion
  let includeStats = $state(false);
```

**Context Used:** AGENTS.md ([source](https://github.com/hackclub/hackatime/blob/main/AGENTS.md))

**Knowledge Base Used:** [Heartbeat imports and exports](https://app.greptile.com/mahadk/-/custom-context/knowledge-base/hackclub/hackatime/-/docs/heartbeat-imports-and-exports.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

operating_system_stats: Heartbeat.attributed_durations_by(scope, :operating_system),
category_stats: Heartbeat.attributed_durations_by(scope, :category),

weekly_project_stats: weekly_project_stats(user: user, scope: scope, start_date: start_date, end_date: end_date),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unsupported keywords abort stats exports

When an export requests stats, this call passes start_date and end_date to weekly_project_stats, whose signature accepts only user and scope. Ruby raises an unknown-keyword ArgumentError, so stats generation stops before the files are added to the archive.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/services/dashboard_data/snapshots.rb
Line: 257

Comment:
**Unsupported keywords abort stats exports**

When an export requests stats, this call passes `start_date` and `end_date` to `weekly_project_stats`, whose signature accepts only `user` and `scope`. Ruby raises an unknown-keyword `ArgumentError`, so stats generation stops before the files are added to the archive.

**Knowledge Base Used:**
- [Dashboards and user insights](https://app.greptile.com/mahadk/-/custom-context/knowledge-base/hackclub/hackatime/-/docs/dashboards-and-user-insights.md)
- [Heartbeat imports and exports](https://app.greptile.com/mahadk/-/custom-context/knowledge-base/hackclub/hackatime/-/docs/heartbeat-imports-and-exports.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.


stats.each do |week, projects|
projects.each do |project, duration|
csv << [week, project, duration]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Weekly projects bypass CSV sanitisation

When a stored heartbeat project begins with a spreadsheet formula prefix, this writer emits the project directly instead of applying the sanitisation used by stats_to_csv, leaving a formula-active cell in weekly_project_stats.csv when the export is opened in spreadsheet software.

How this was verified: Stored project values retain formula prefixes, flow into weekly project keys and are written here without the adjacent CSV sanitiser.

Suggested change
csv << [week, project, duration]
safe_project = project.to_s.gsub(/\A([=+\-@\t\r])/, "'\1")
csv << [week, safe_project, duration]

Knowledge Base Used: Heartbeat tracking and ingestion

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/jobs/heartbeat_export_job.rb
Line: 154

Comment:
**Weekly projects bypass CSV sanitisation**

When a stored heartbeat project begins with a spreadsheet formula prefix, this writer emits the project directly instead of applying the sanitisation used by `stats_to_csv`, leaving a formula-active cell in `weekly_project_stats.csv` when the export is opened in spreadsheet software.

**How this was verified:** Stored project values retain formula prefixes, flow into weekly project keys and are written here without the adjacent CSV sanitiser.

```suggestion
          safe_project = project.to_s.gsub(/\A([=+\-@\t\r])/, "'\1")
          csv << [week, safe_project, duration]
```

**Knowledge Base Used:** [Heartbeat tracking and ingestion](https://app.greptile.com/mahadk/-/custom-context/knowledge-base/hackclub/hackatime/-/docs/heartbeat-tracking.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +76 to +83
test "POST export enqueues job with include_stats when checked" do
assert_enqueued_with(
job: HeartbeatExportJob,
args: [@user.id, hash_including(include_stats: true, all_data: true)]
) do
post export_my_heartbeats_url, params: { all_data: "1", include_stats: "1" }
end
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stats controller test cannot run

When this test runs, it dereferences an uninitialised @user before posting the request. Even after initialising the user, the submitted values are "1" while the controller enables both options only for "true", so the test cannot reach or verify the stats-enabled enqueue path.

Context Used: In the Hackatime repo, please ensure that the PR i... (source)

Knowledge Base Used: Heartbeat imports and exports

Prompt To Fix With AI
This is a comment left during a code review.
Path: test/controllers/my/heartbeats_controller_test.rb
Line: 76-83

Comment:
**Stats controller test cannot run**

When this test runs, it dereferences an uninitialised `@user` before posting the request. Even after initialising the user, the submitted values are `"1"` while the controller enables both options only for `"true"`, so the test cannot reach or verify the stats-enabled enqueue path.

**Context Used:** In the Hackatime repo, please ensure that the PR i... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

**Knowledge Base Used:** [Heartbeat imports and exports](https://app.greptile.com/mahadk/-/custom-context/knowledge-base/hackclub/hackatime/-/docs/heartbeat-imports-and-exports.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant