どのような時間配分でプログラミングや仕事をしているのだろう

僕はソフトウェア開発と書籍の執筆、翻訳、監訳を生業としている。そのため日常的にプログラミングをするし、コンピュータ上の文章を書いたり触れたりする時間は長い。作業効率を考える上で、具体的に作業のどの部分に時間をかけているかを知る必要がある。そこで今回は作業時間を計測し、計測した結果を分析できるように工夫してみる事にする。

org-clockとwakatime

ソフトウェアの開発というと、代表的な作業はプログラミングだろうか。当然、僕も日常的にプログラミングをしている。プログラミングに使うツールは、開発対象や環境、またはプログラマーの好みによって変わる。僕はGNU Emacsというテキストエディタを愛用していて、プログラミングに関わる多くの作業でそれを使っている。プログラミング以外にも、仕様書の作成、作図など様々な作業でそれを使う。それと同様に書籍に関連する多くの作業にもEmacsを使う。

Emacsにはorg-modeというものがある。これはマークアップ言語であり、一連の機能を備えたドキュメントシステムだ。文章の作成、表計算、タスク管理など多くの機能を含んでいる。僕はタスク管理にこのorg-modeを使っている。org-modeのタスク管理には、時間計測の機能である org-clock も備わっている。そのため各タスクにかかった作業時間を計測できるようになっている。しかし org-clock だけでは、どの種類のファイルの編集にどの程度の時間を使っているか等については計測できない。

どの種類のファイルの編集にどの程度時間を使っているのかを記録するには一工夫必要になるが、ちょうどその機能を提供しているWebサービスがある。それが wakatime だ。

Emacsでwakatimeを使う

wakatime-mode

wakatime 3は作業時間を計測し集計可視化するWebサービスであり、様々な環境向けの拡張を提供している。その中にはEmacs向けの拡張として wakatime-mode を提供しており、特別な理由がなければそれを使う事で、作業時間を自動的に収集し、wakatime上で可視化してくれる4wakatime-mode 自体は MELPA からインストールできる。

M-x package-install RET wakatime-mode

wakatime-mode は、Pythonで実装されたCLIコマンド wakatime を利用してデータを送信する実装となってい5。そのため pip などを利用して wakatime パッケージをインストールする必要がある。

pip install wakatime

wakatime の設定は .wakatime.cfg に記述する。 `api_key` にwakatimeのサイトで発行したapi_keyを設定する。

[settings]
debug = false
api_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
hidefilenames = false
exclude =
    ^COMMIT_EDITMSG$
    ^TAG_EDITMSG$
    ^/var/(?!www/).*
    ^/etc/
include =
    .*
offline = true
timeout = 30
$HOME/.wakatime.cfg

そして global-wakatime-mode を有効にする事で、データの記録を行う事ができる。

(global-wakatime-mode t)

この状態で Git リポジトリ内のファイルを開くと、リポジトリをプロジェクトと見立てて作業時間を計測してくれる。

ファイルではないバッファに対して時間の計測を行う1

wakatime-mode は開いているファイルに対して、プロジェクトでの作業時間の計測をするが、ファイルではないバッファに対しては集計してくれない。ファイルじゃないバッファの場合も、プロジェクトで作業していると考えるなら、作業時間として計測したい。そこでバッファ名をプロジェクトとしてみたてるようにカスタマイズする6

wakatime-client-commandwakatime コマンドを生成する。これを上書きしてファイル名がなかったら、バッファ名を用いるように変更している。

(defun wakatime-client-command (savep)
  "Return client command executable and arguments.
 Set SAVEP to non-nil for write action."
  (format "%s%s--file \'%s\' %s --plugin \"%s/%s\" --time %.2f%s%s"
          (if (s-blank wakatime-python-bin) "" (format "%s " wakatime-python-bin))
          (if (s-blank wakatime-cli-path) "wakatime " (format "%s " wakatime-cli-path))
          (or (buffer-file-name (current-buffer)) (buffer-name))
          (if (buffer-file-name (current-buffer)) "" (format " --entity-type app --project \'%s\' " mode-name))
          wakatime-user-agent
          wakatime-version
          (float-time)
          (if savep " --write" "")
          (if (s-blank wakatime-api-key) "" (format " --key %s" wakatime-api-key))))

(defun wakatime-save ()
  "Send save notice to WakaTime."
  (wakatime-call t))

(defun wakatime-bind-hooks ()
  "Watch for activity in buffers."
  (add-hook 'after-save-hook 'wakatime-save nil t)
  (add-hook 'auto-save-hook 'wakatime-save nil t)
  (add-hook 'first-change-hook 'wakatime-ping nil t))

(defun wakatime-unbind-hooks ()
  "Stop watching for activity in buffers."
  (remove-hook 'after-save-hook 'wakatime-save t)
  (remove-hook 'auto-save-hook 'wakatime-save t)
  (remove-hook 'first-change-hook 'wakatime-ping t))

プロジェクトやカテゴリーの値を設定する

この wakatime コマンドは、ファイルがあるディレクトリからリポジトリなどの情報を用いてプロジェクトごとの時間を計測している。しかし、それでは都合が悪い事もある。例えばマルチレポ戦略を取っている開発チームの場合、同一プロジェクトでも複数のGitリポジトリを持っている。この複数のGitリポジトリを別々として集計したいのであれば、問題はないけれど同一プロジェクトとして集計したいなら工夫が必要になる。プロジェクトやカテゴリーの値は org-clock の提供するコマンド org-clock-in で集計を開始した org-todo のタスクの属性から取得したい。

wakatimeにデータを送信するために独自の拡張を書く

Python 製の wakatime コマンドと、公式のEmacs拡張である wakatime-mode を調整し、それを実現する事もできるかもしれない。ただ、それよりも自前で実装しEmacsから直接制御したほうが、見通しがよくなると考えた。また実装の見通しを良くするために、データをローカルにキャッシュする部分であある wakatime-record.el と、キャッシュしたデータをwakatimeに送信する部分である wakatime-transport.el に分けて実装した。またorg-modeとの橋渡しとして org-wakatime.el も実装した。

wakatimeへ送信するデータをローカルにキャッシュする

wakatime-record.el は計測した結果をローカルのファイルにキャッシュする。 wakatime へのデータの送信は行わない。 (wakatime-record-tunrn-on) することで有効になる。

(require 'wakatime-record)

(wakatime-record-tunrn-on)

wakatime-record.el

;;; wakatime-record.el --- Yet Another Wakatime plugin for Emacs.

;; Copyright (C) 2021 TakesxiSximada <[email protected]>

;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; Website: https://github.com/TakesxiSximada/emacs.d/master/wakatime/
;; Keywords: calendar, comm
;; Package-Version: 20210730.240
;; Package-Commit: 5e6deddda7a70f4b79832e9e8b6636418812e162
;; Version: 1.0.0

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU Affero General Public License for more details.

;; You should have received a copy of the GNU Affero General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; wakatime-record.el is an unofficial wakatime plugin for Emacs.
;; It was implemented based on a different design philosophy.
;; Heartbeats are recorded as Line-delimited JSON in a temporary journal.

;;; Code:
(require 'cl-lib)
(require 'json)
(require 'org-clock)

(defvar wakatime-record-buffer-name "*WAKATIME RECORD*")
(defvar wakatime-record-file-path (expand-file-name "~/.wakatime.heartbeat.json"))
(defvar wakatime-record-timer nil)

(setq wakatime-record-language-alist
      '(
	(Info-mode . "Emacs")
	(c-mode . "C")
	(compilation-mode . "Completion")
	(completion-list-mode . "Completion")
	(conf-toml-mode . "TOML")
	(css-mode . "CSS")
	(dired-mode . "Dired")
	(dockerfile-mode . "Docker")
	(editor-mode . "Org")
	(emacs-lisp-mode . "Emacs Lisp")
	(foreman-mode . "Foreman")
	(fundamental-mode . "Emacs Lisp")
	(go-mode . "Go")
	(html-mode . "HTML")
	(js-mode . "JavaScript")
	(js2-mode . "JavaScript")
	(json-mode . "JSON")
	(lisp-interaction-mode . "Emacs Lisp")
	(magit-refs-mode . "Git")
	(magit-status-mode . "Git")
	(messages-buffer-mode . "Emacs")
	(mhtml-mode . "HTML")
	(org-agenda-mode . "Org")
	(org-mode . "Org")
	(python-mode . "Python")
	(restclient-mode . "HTTP")
	(rustic-mode . "Rust")
	(scss-mode . "CSS")
	(shell-script-mode . "Sehll")
	(sql-interactive-mode . "SQL")
	(sql-mode . "SQL")
	(typescript-mode . "TypeScript")
	(vterm-mode . "Shell")
	(vue-html-mode . "Vue")
	(vue-mode . "Vue")
	(xwidget-webkit-mode . "HTML")
	(yaml-mode . "YAML")
	))

(setq wakatime-record-category-alist
      '(
	(Info-mode . "coding")
	(c-mode . "coding")
	(compilation-mode . "building")
	(compilation-mode . "coding")
	(completion-list-mode . "coding")
	(conf-toml-mode . "coding")
	(css-mode . "coding")
	(dired-mode . "planning")
	(dockerfile-mode . "coding")
	(editor-mode . "writing docs")
	(emacs-lisp-mode . "coding")
	(eww-mode . "eww-mode")
	(fundamental-mode . "coding")
	(go-mode . "coding")
	(html-mode . "coding")
	(js-mode . "coding")
	(js2-mode . "coding")
	(json-mode . "coding")
	(lisp-interaction-mode . "coding")
	(magit-refs-mode . "coding")
	(magit-status-mode . "coding")
	(messages-buffer-mode . "planning")
	(mhtml-mode . "coding")
	(org-agenda-mode . "planning")
	(org-mode . "writing docs")
	(python-mode . "coding")
	(restclient-mode . "coding")
	(rustic-mode . "coding")
	(scss-mode . "coding")
	(shell-script-mode . "coding")
	(sql-interactive-mode . "coding")
	(sql-mode . "coding")
	(typescript-mode . "coding")
	(vterm-mode . "coding")
	(vue-html-mode . "coding")
	(vue-mode . "coding")
	(xwidget-webkit-mode . "coding")
	(yaml-mode . "coding")
	(nil . "code reviewing")
	(nil . "debugging")
	(nil . "designing")
	(nil . "indexing")
	(nil . "learning")
	(nil . "manual testing")
	(nil . "meeting")
	(nil . "researching")
	(nil . "running tests")
	(nil . "writing tests")))


(cl-defstruct wakatime-record-heartbeat
  time
  user_agent
  entity
  type
  category
  is_write
  project
  ;; branch
  language
  ;; dependencies
  ;; lines
  ;; lineno
  ;; cursorpos
  )

(defun wakatime-record-get-category-by-major-mode ()
  (or
   (cdr (assoc major-mode wakatime-record-category-alist))
   "planning"))

(defalias 'wakatime-record-get-category 'wakatime-record-get-category-by-major-mode)

(defun make-wakatime-record-current-heartbeat ()
  (make-wakatime-record-heartbeat
   :time (float-time)
   :type "file"
   :user_agent "emacs"
   :entity (buffer-name)
   :language (or (cdr (assoc major-mode wakatime-record-language-alist)) major-mode)
   :project (if-let ((current-task-buffer (org-clock-is-active)))
		(with-current-buffer current-task-buffer
		  (org-get-category))
	      "GLOBAL")
   :is_write t
   :category (wakatime-record-get-category)
   ))

(defun wakatime-record-serialize (heatbeat)
  (concat
   (json-encode-alist
    (let ((typ (type-of heatbeat)))
      (mapcar (lambda (key) `(,key . ,(cl-struct-slot-value typ key heatbeat)))
	      (mapcar 'car (cdr (cl-struct-slot-info typ))))))
   "\n"))


(defun wakatime-record-save-heatbeat ()
  (interactive)
  (let ((serialized-heatbeat (wakatime-record-serialize
			      (make-wakatime-record-current-heartbeat))))
    (with-current-buffer (get-buffer-create wakatime-record-buffer-name)
      (insert serialized-heatbeat)
      (write-region (point-min) (point-max)
		    wakatime-record-file-path
		    t)
      (kill-buffer))))

(defun wakatime-record-tunrn-off ()
  (interactive)
  (when (timerp wakatime-record-timer)
    (cancel-timer wakatime-record-timer)))


(defun wakatime-record-tunrn-on ()
  (interactive)
  (wakatime-record-tunrn-off)
  (setq wakatime-record-timer
	(run-with-idle-timer 20 t #'wakatime-record-save-heatbeat)))

(provide 'wakatime-record)
;;; wakatime-record.el ends here

wakatimeへデータを送信する

wakatime-transport.elwakatime-record.el がキャッシュしたデータをwakatimeへ送信します。 (wakatime-transport-turn-on)) することで有効になります。

(require 'wakatime-transport)

(wakatime-transport-turn-on)

wakatime-transport.el

;;; wakatime-transport.el --- Yet Another Wakatime plugin for Emacs.

;; Copyright (C) 2021 TakesxiSximada <[email protected]>

;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; Website: https://github.com/TakesxiSximada/emacs.d/master/wakatime/
;; Keywords: calendar, comm
;; Package-Version: 20210730.240
;; Package-Commit: 5e6deddda7a70f4b79832e9e8b6636418812e162
;; Version: 1.0.0

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU Affero General Public License for more details.

;; You should have received a copy of the GNU Affero General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; wakatime-transport.el is Unofficial wakatime plugin for Emacs.  
;; It was implemented based on a different design philosophy.
;; wakatime-transport.el is an unofficial wakatime plugin for
;; Emacs. Send the Heartbeat Journal to https://wakatime.com. This
;; package does not save the heatbeat journal, wakatime-record.el does
;; it instead.

;;; Code:
(require 'json)
(require 'seq)
(require 'timer)

(require 'restclient)

(defvar wakatime-transport-buffer-name "*WAKATIME TRANSPORT*")
(defvar wakatime-transport-file-path (expand-file-name "~/.wakatime.heartbeat.json"))
(defvar wakatime-transport-response-buffer nil)
(defvar wakatime-transport-restclient-file (expand-file-name
					    "~/.emacs.d/wakatime.heartbeat.bulk.http"))
(defvar wakatime-transport-timer nil)

(defun wakatime-transport-get-heartbeeats-cache ()
  (json-encode-array
   (mapcar #'json-parse-string
	   (seq-filter
	    (lambda (elt) (> (length elt) 0))
	    (split-string
	     (with-current-buffer (get-buffer-create wakatime-transport-buffer-name)
	       (buffer-substring-no-properties (point-min) (point-max)))
	     "\n")))))

(defun wakatime-transport-send-request ()
  (when (buffer-live-p wakatime-transport-response-buffer)
    (let ((kill-buffer-query-functions nil))
      (kill-buffer wakatime-transport-response-buffer)))

  (setq wakatime-transport-response-buffer
	(with-current-buffer (find-file-noselect wakatime-transport-restclient-file)
	  (restclient-http-send-current-stay-in-window))))


(defun wakatime-transport-load-heatbeats ()
  (let ((buf (get-buffer-create wakatime-transport-buffer-name)))
    (with-current-buffer buf (erase-buffer))

    (make-process
     :name "WAKATIME TRANSPORT"
     :buffer buf
     :command `("/usr/local/bin/gsed" "-i"
      		"-e" "1,10 w /dev/stdout"
    		"-e" "1,10d"
		,wakatime-transport-file-path))))

(defun wakatime-transport-heartbeats ()
  (let ((proc (wakatime-transport-load-heatbeats)))
    (set-process-sentinel
     proc (lambda (proc sig)
	    (wakatime-transport-send-request)))))


(defun wakatime-transport-turn-off ()
  (interactive)
  (when (timerp wakatime-transport-timer)
    (cancel-timer wakatime-transport-timer)))


(defun wakatime-transport-turn-on ()
  (interactive)
  (wakatime-transport-turn-off)
  (setq wakatime-transport-timer
	(run-with-idle-timer 60 t #'wakatime-transport-heartbeats)))

(provide 'wakatime-transport)
;;; wakatime-transport.el ends here

org-modeとの橋渡し

org-mode のプロパティに wakatime のカテゴリーを登録するための関数を実装する。 wakatime-record-get-category 関数を上書きすることで org-mode のプロパティからカテゴリーの取得を試みる。設定されていなければ、メジャーモードからカテゴリーを推測する。

(require 'org-wakatime)

(defun wakatime-record-get-category ()
  (interactive)
  (or (org-wakatime-get-category)
      (wakatime-record-get-category-by-major-mode)))

org-wakatime.el

;;; org-wakatime.el --- Wakatime Org Mode Support.

;; Copyright (C) 2021 TakesxiSximada <[email protected]>

;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; Website: https://github.com/TakesxiSximada/emacs.d/master/wakatime/
;; Keywords: calendar, comm
;; Package-Version: 20210730.240
;; Package-Commit: 5e6deddda7a70f4b79832e9e8b6636418812e162
;; Version: 1.0.0

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU Affero General Public License for more details.

;; You should have received a copy of the GNU Affero General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; org-wakatime.el is an unofficial package related to wakatime.  It save
;; category for wakatimed in the org property. wakatime-record.el can
;; detect the current working category from there.

;;; Code:
(defvar org-wakatime-category-property-name "WAKATIME_CATEGORY")

(defun org-wakatime-set-category (wakatime-category)
  (interactive (list (completing-read
		      "WAKATIME_CATEGORY: "
		      (delete-dups (mapcar #'cdr wakatime-record-category-alist)))))
  (org-set-property org-wakatime-category-property-name
		    wakatime-category))

(defun org-wakatime-get-category ()
  (interactive)
  (if-let ((current-task-buffer (org-clock-is-active)))
      (with-current-buffer current-task-buffer
	(save-excursion
	  (goto-char (marker-position org-clock-marker))
	  (cdr (assoc org-wakatime-category-property-name (org-entry-properties)))))))

(provide 'org-wakatime)
;;; org-wakatime.el ends here

wakatimeのハートビートに設定するprojectをorg-clockのカレントタスクから取得する

取得する関数を定義する7

(defun waka-get-project ()
  (interactive)
  (when org-clock-marker
    (with-current-buffer (marker-buffer org-clock-marker)
      (org-get-category))))

その関数をrestclientで利用する。 先程定義したwaka-get-projectで取得した値を restclient内で :PROJECT に設定している。

# -*- restclient -*-
:ORIGIN := "https://wakatime.com"
:API_KEY := (base64-encode-string (getenv+ "WAKATIME_API_KEY"))
:TYPE := "file"
:CATEGORY := "coding"
:TIMESTAMP := (float-time)
:LANGUAGE := "org-mode"
:ENTITY := (buffer-name)
:PROJECT := (waka-get-project)

POST :ORIGIN/api/v1/users/current/heartbeats
Authorization: Basic :API_KEY
Content-Type: application/json

{
  "entity": ":ENTITY",
  "project": ":PROJECT",
  "category": ":CATEGORY",
  "language": ":LANGUAGE",
  "time": :TIMESTAMP,
  "is_write": true
}

以下の結果が返される。

#+BEGIN_SRC js
{
  "data": {
    "branch": null,
    "category": "coding",
    "created_at": "2021-07-28T20:24:00Z",
    "cursorpos": null,
    "dependencies": [],
    "entity": "*temp*-271500",
    "id": "33ca1dcd-1a81-44fd-baea-7575bd3dc1ab",
    "is_write": true,
    "language": "org-mode",
    "lineno": null,
    "lines": null,
    "machine_name_id": null,
    "project": "symdon",
    "time": 1627503839.625936,
    "type": "file",
    "user_agent_id": null,
    "user_id": "71580bdd-fd95-41a2-babd-182b9a930d8b"
  }
}

// POST https://wakatime.com/api/v1/users/current/heartbeats
// HTTP/1.1 201 CREATED
// Server: nginx
// Date: Wed, 28 Jul 2021 20:24:00 GMT
// Content-Type: application/json
// Content-Length: 401
// Connection: keep-alive
// Access-Control-Allow-Methods: GET, PUT, OPTIONS, PATCH, HEAD, POST
// Access-Control-Max-Age: 21600
// Access-Control-Allow-Origin: *
// X-Content-Type-Options: nosniff
// Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
// X-XSS-Protection: 1; mode=block
// Expect-CT: enforce, max-age=1209600, report-uri='https://3bca3d665311907a9472c2b8373ab3f6.report-uri.com/r/d/ct/enforce'
// Feature-Policy: accelerometer 'none';autoplay 'self';camera 'none';document-domain 'none';fullscreen 'self';geolocation 'none';gyroscope 'none';magnetometer 'none';microphone 'none';midi 'none';payment 'self';picture-in-picture 'none';sync-xhr 'self';usb 'none';
// Referrer-Policy: strict-origin-when-cross-origin
// X-Frame-Options: SAMEORIGIN
// Content-Security-Policy: default-src 'self'; frame-ancestors 'self'; script-src 'self' 'unsafe-eval' https://js.stripe.com https://*.braintreegateway.com https://api.github.com https://www.google.com/ https://www.gstatic.com/ https://www.googletagmanager.com https://www.google-analytics.com https://heapanalytics.com https://*.heapanalytics.com; img-src 'self' data: https://cdn.loom.com/ https://pbs.twimg.com https://checkout.paypal.com https://*.braintreegateway.com heapanalytics.com; style-src 'self' 'unsafe-inline'; media-src 'self' https://*.amazonaws.com; frame-src 'self' https://www.google.com/ https://js.stripe.com/ https://hooks.stripe.com/ https://www.youtube.com/ https://www.loom.com/ player.vimeo.com checkout.paypal.com; object-src 'self'; connect-src 'self' api.github.com https://www.google.com/ www.google-analytics.com heapanalytics.com https://avatar-cdn.atlassian.com https://api.stripe.com;
// Request duration: 0.678229s
#+END_SRC

wakatimeのハートビートを手動で送信する

restclientを使ってwakatimeのハートビートの送信方法を確認確認した。 認証方法はBasic認証でAPIキーを用いる簡易な方法を用いた。 APIキーの作成は https://wakatime.com/settings/api-key から行える。

:ORIGIN := "https://wakatime.com"
:API_KEY := (base64-encode-string (getenv "WAKATIME_API_KEY"))
:TYPE := "file" ;; app file domain
:CATEGORY := "learning" ;;
:TIMESTAMP := (float-time)

POST :ORIGIN/api/v1/users/current/heartbeats
Authorization: Basic :API_KEY
Content-Type: application/json

{
  "entity": "foo",
  "project": "symdon",
  "category": ":CATEGORY",
  "language": "org-mode",
  "time": :TIMESTAMP,
  "is_write": true
}
{
  "data": {
    "branch": null,
    "category": "learning",
    "created_at": "2021-07-25T23:43:13Z",
    "cursorpos": null,
    "dependencies": [],
    "entity": "foo",
    "id": "0bff3974-cbf2-4246-84de-14849ad3d357",
    "is_write": true,
    "language": "org-mode",
    "lineno": null,
    "lines": null,
    "machine_name_id": null,
    "project": "symdon",
    "time": 1627256593.444148,
    "type": "file",
    "user_agent_id": null,
    "user_id": "71580bdd-fd95-41a2-babd-182b9a930d8b"
  }
}

// POST https://wakatime.com/api/v1/users/current/heartbeats
// HTTP/1.1 201 CREATED
// Server: nginx
// Date: Sun, 25 Jul 2021 23:43:13 GMT
// Content-Type: application/json
// Content-Length: 393
// Connection: keep-alive
// Access-Control-Allow-Methods: GET, PUT, OPTIONS, PATCH, HEAD, POST
// Access-Control-Max-Age: 21600
// Access-Control-Allow-Origin: *
// X-Content-Type-Options: nosniff
// Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
// X-XSS-Protection: 1; mode=block
// Expect-CT: enforce, max-age=1209600, report-uri='https://3bca3d665311907a9472c2b8373ab3f6.report-uri.com/r/d/ct/enforce'
// Feature-Policy: accelerometer 'none';autoplay 'self';camera 'none';document-domain 'none';fullscreen 'self';geolocation 'none';gyroscope 'none';magnetometer 'none';microphone 'none';midi 'none';payment 'self';picture-in-picture 'none';sync-xhr 'self';usb 'none';
// Referrer-Policy: strict-origin-when-cross-origin
// X-Frame-Options: SAMEORIGIN
// Content-Security-Policy: default-src 'self'; frame-ancestors 'self'; script-src 'self' 'unsafe-eval' https://js.stripe.com https://*.braintreegateway.com https://api.github.com https://www.google.com/ https://www.gstatic.com/ https://www.googletagmanager.com https://www.google-analytics.com https://heapanalytics.com https://*.heapanalytics.com; img-src 'self' data: https://cdn.loom.com/ https://pbs.twimg.com https://checkout.paypal.com https://*.braintreegateway.com heapanalytics.com; style-src 'self' 'unsafe-inline'; media-src 'self' https://*.amazonaws.com; frame-src 'self' https://www.google.com/ https://js.stripe.com/ https://hooks.stripe.com/ https://www.youtube.com/ https://www.loom.com/ player.vimeo.com checkout.paypal.com; object-src 'self'; connect-src 'self' api.github.com https://www.google.com/ www.google-analytics.com heapanalytics.com https://avatar-cdn.atlassian.com https://api.stripe.com;
// Request duration: 0.156332s

各フィールド2

FieldTypeMustDescription
entitystringmustエンティティのハートビートは、絶対ファイルパスやドメインなどに対して時間を記録しています
typestringoptionalエンティティのタイプ。ファイル、アプリ、またはドメインにすることができます
categorystringoptionalこのアクティビティのカテゴリ。通常、これは型から自動的に推測されます。コーディング、ビルド、インデックス作成、デバッグ、ブラウジング、テストの実行、テストの作成、手動テスト、ドキュメントの作成、コードレビュー、調査、学習、または設計が可能です。
timefloatmustUNIXエポックタイムスタンプ。小数点以下の数値は秒の端数です>、
projectstringoptionalプロジェクト名
branchstringoptionalブランチ名
languagestringoptionalプログラミング言語など
dependenciesstringoptionalエンティティファイルから検出された依存関係のコンマ区切りリスト
linesintegerwhen entity type is fileエンティティ内の行の総数
linenointegeroptionalカーソルの現在の行行番号
cursorposintegeroptional現在のカーソル列の位置
is_writebooleanoptionalこのハートビートがファイルへの書き込みからトリガーされたかどうか