Jira already knows what you worked on: rebuilding worklogs from issue history
DEV Community

Jira already knows what you worked on: rebuilding worklogs from issue history

Logging time in Jira was always the thing I put off. Then the end of the month came, and I sat there trying to remember what I did two weeks ago, opening tickets one by one and guessing the hours.

At some point I noticed that Jira already has most of the answer. Every time I move a ticket to In Progress, or someone assigns it to me, Jira writes that down in the issue changelog. So I wrote a small Chrome extension that reads that history and turns it into worklog drafts. I check them, fix what's wrong and post.

This post is about how that works, because a few parts turned out more interesting than I expected.

The idea

If a ticket was assigned to me and sat in a "working" status from 10:00 to 15:00, I was probably working on it then. That's a guess, not tracked time. Nothing watches my screen or counts keystrokes.

So nothing gets posted automatically either. Everything lands in a calendar as a draft first.

You pick which statuses count as "working". Usually something like In Progress and In Review.

Step 1: what did the ticket look like at any moment?

The changelog (/rest/api/3/issue/{id}/changelog) only stores changes: field, from, to and a timestamp. It doesn't tell you what the status was at the start of the range you care about.

The trick is to start from the current state, which you do know, and walk the history backwards, undoing each change with its from value. Then you know the state at the very beginning. After that you walk forward and record every stretch where both things are true: the ticket is mine, and it's in a working status.

const state = { status: issue.fields.status.id, assignee: issue.fields.assignee?.accountId, };
// undo every change to get the state before the first one
for (const e of [...events].reverse()) state[e.field] = e.from;
let cursor = start;
for (const e of events) {
  const active = state.assignee === me && workingStatuses.includes(state.status);
  if (active && e.at > cursor) intervals.push([cursor, e.at]);
  cursor = e.at;
  state[e.field] = e.to;
}

The real version also clips everything to the date range, but this is the core. Each ticket ends up with a list of [start, end] timestamps.

Step 2: turning intervals into hours

Raw intervals are useless on their own. A ticket left In Progress over the weekend would get 48 hours. So each day is clipped to working windows from the settings: 9:00 to 13:00 and 14:00 to 18:00 by default, no weekends, and today stops at "now".

Then the annoying case: two or three tickets in progress at the same time. I cut the day at every point where any ticket starts or stops, and split each slice equally between the tickets that were active in it.

for (const [start, end] of windows) {
  const points = [...new Set([start, end, ...boundariesInside(start, end)])].sort((a, b) => a - b);
  for (let i = 1; i < points.length; i++) {
    const [a, b] = [points[i - 1], points[i]];
    const active = sources.filter((s) => s.intervals.some(([x, y]) => x <= a && y >= b));
    for (const s of active) totals[s.issue.id] += (b - a) / 1000 / active.length;
  }
}

So if ticket A was active all morning and B joined at 11:00, A gets 9 to 11 alone plus half of 11 to 13. It's simple, and in practice close enough.

Time already logged in Jira for that day is subtracted first, and the day is capped at 8 hours (configurable).

Step 3: rounding without breaking the total

Nobody wants a worklog of 2h 37m 12s, so drafts are rounded to 30 minutes. But rounding each entry on its own breaks the day. Three tickets with 2h 40m each, rounded up, give you 9 hours instead of 8.

So I round the day total first. Each entry gets its rounded-down value, and the leftover 30-minute blocks go to the entries with the biggest remainders. In the example that's 3h, 2h 30m and 2h 30m, exactly 8 hours.

It's the largest remainder method, the same one some countries use to split parliament seats. Small thing, but I liked it.

Step 4: no API tokens

Most Jira tools want an API token, or an OAuth app that an admin has to approve. I didn't want either. When you connect, the extension asks for permission to your *.atlassian.net site, and then simply calls the REST API with the session you already have in the browser:

const response = await fetch(`${origin}${path}`, {
  method,
  credentials: 'include',
  redirect: 'error',
  headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});

One guard I added later: before any write it calls /rest/api/3/myself and compares the account ID. If you switched Jira accounts in another tab, posting stops instead of logging time as someone else.

Step 5: never post the same worklog twice

This is the part I spent the most time on. You press Submit, the POST goes out, and the connection drops before the response comes back. Did Jira create the worklog or not? Retry, and you might get a duplicate. Don't, and you might lose it.

Two things solve it:

  • The draft is saved as posting in local storage before the request is sent.
  • The worklog itself carries an entity property with the draft ID. Jira lets you attach it in the same POST.
await request(c, `/rest/api/3/issue/${d.issueId}/worklog?adjustEstimate=leave`, 'POST', {
  ...worklogBody(d),
  properties: [{ key: 'coma-log.draft', value: { id: d.id } }],
});

On the next sync, any draft stuck in posting becomes unknown. For worklogs with the same issue, date and duration, the extension reads that property. If the ID matches, the draft is marked as posted. If nothing matches, the draft stays locked and you check it in Jira yourself. It is never retried blindly.

Where it doesn't work

  • If you don't move tickets through statuses, there's nothing to rebuild. Jira history is the main input. Calendar meetings and GitHub reviews can be added, but they're optional.
  • It's an estimate. Three days In Progress doesn't mean three days of work, which is why everything stays a draft until you submit.
  • Jira Cloud only, Chrome only, one account at a time.
  • Some companies block third-party extensions, and then there's nothing to do.

That's it

I built it for myself, and now worklogs take me a few minutes of checking drafts instead of an evening of guessing. It's free, there's no backend, and drafts stay in your browser.

If you also hate filling in worklogs, it's here: Snaplog for Jira. And if your team uses Jira statuses in a way that would break this logic, I'd like to hear about it in the comments.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.