Fetch log
The record of every automatic fetch: what a row means, success vs failure vs skipped, the expanded detail, filters, retention and how to debug a broken source.
Every automatic fetch Pulsaria runs is written to a log before anything else happens — successes, failures and deliberately skipped attempts alike. When a KPI stops updating or a number looks wrong, this is the screen that tells you why.
Where it is
Fetch Log is the fifth of the six top-level entries in the sidebar, between Reports and App Settings, on the route /logs. On a narrow window it is one of the six tabs in the bottom bar. Two things also link straight here: the red toast a failed scheduled fetch raises (“Auto-KPI failed” with a View details action), and the failure toast from a manual Run now (“Auto-KPI failed”, also with View details).
The heading is Fetch Log, and under it sits the subtitle “Recent auto-KPI fetches (Bash & HTTP). Max 500 entries.”
The subtitle says Bash and HTTP, but connector fetches are logged here too. That wording is out of date, not the behaviour.
What lands in the log
Only automatic sources write to it — Bash, HTTP and connector KPIs. Manual KPIs never appear, and neither does anything you type on the Daily Entry screen.
Every attempt from these three paths is recorded:
- The background scheduler, when a KPI’s cron expression fires.
- Run now on a KPI card or the KPI detail page.
- Refresh in the Dashboard header, which runs all automatic KPIs at once.
Run test in the KPI dialog and Run test in the connector setup wizard are not logged. They run the same request and show you the same information in the result panel below the button, but they write nothing to disk.
What a row shows
Rows are newest first. Each collapsed row reads, left to right:
| Element | What it shows |
|---|---|
| Chevron | Points right when collapsed, rotates down when the row is open |
| Status icon | A green check when a value was stored, a red cross otherwise |
| KPI name | The KPI’s name as it was at the moment of the fetch |
| Second line | On success Value: 1234. On failure the error message, in red monospace |
| Source badge | BASH, HTTP or CONNECTOR |
| Duration | For example 412 ms — how long the whole attempt took |
| Time | Relative: “just now”, “5 minutes ago”, “3 hours ago”, “2 days ago”. Past seven days it switches to a plain date |
The name is a snapshot. Rename a KPI and old rows keep the old name; delete a KPI and its rows stay in the log.
Success, failure and skipped
A row is green only when a number was parsed and written to the KPI’s history for today. There is no partial outcome: a failed fetch stores no value, leaves yesterday’s number in place, and does not touch the KPI’s status.
A row is red in two different situations that the screen does not distinguish:
Failure — the command exited non-zero, the API returned an error status, the response was not JSON, the JSONPath matched nothing, the output was not a number. Something went wrong.
Skipped — Pulsaria deliberately did not run the fetch. There are exactly two reasons, both about approval:
Command not approved — please confirm in KPI settingsfor a Bash KPI that has never been approved.HTTP config not approved — please confirm in KPI settingsfor a custom HTTP KPI in the same state.
Skipped attempts are stored with their own flag, but the Fetch Log draws them with the same red cross and red text as a real failure, and the status filter counts them under Errors. The one place they get their own treatment is the summary toast after Refresh, which reads “3 successful · 1 failed · 2 skipped” and offers an Approve action. Skipped runs also do not raise the “Auto-KPI failed” toast — only real failures do.
If you see one of those two messages, nothing is broken. Open the KPI, press Run test, then Approve. See Shell commands and Custom HTTP sources for the approval model.
Expanding a row
Click anywhere on a row to open it. The detail area shows, in this order:
- The absolute date and time of the attempt, formatted for your language.
HTTP Status: 200— only for HTTP and connector fetches.- A boxed, monospace
stdoutblock — Bash only, and only when the command printed something. - A boxed
stderrblock in red — Bash only, only when non-empty. - A boxed
bodyblock — the response body of an HTTP or connector fetch.
Each box scrolls on its own once it gets tall, and long lines wrap. Several rows can be open at the same time; the state resets when you leave the screen.
What each source records
| Source | Recorded in the file |
|---|---|
| Bash | stdout, stderr, exit code, the command as executed, the shell invocation |
| HTTP | Status code, response body (first 4096 characters), the URL as dispatched, the method |
| Connector | Status code, response body (first 4096 characters), the resolved URL, the method |
The expanded panel shows only the timestamp, the HTTP status, stdout, stderr and the body. The exit code, the executed command, the shell, the effective URL and the HTTP method are written to the file but are not displayed anywhere in the app. To read them, open
fetch-log.jsonyourself or take a JSON export.
For a Bash failure the exit code is at least visible indirectly: the error message on the row itself reads Exit code 1. And if you need the exact command and shell while you are still building a KPI, the Run test result panel in the KPI dialog does show them.
The body is truncated twice on the way in: the native HTTP layer caps any response at 64 KB and gives up after 10 seconds, then Pulsaria stores the first 4096 characters of what came back. A large JSON response will be cut off mid-structure in the log. That is expected.
Filters
Three dropdowns sit above the list and combine with AND. The number of matching rows is shown at the right end of the same line, for example 37 entries.
| Filter | Options |
|---|---|
| KPI | All KPIs, plus every non-manual KPI by name |
| Source | All sources, Bash, HTTP |
| Status | All, Success, Errors |
Two gaps worth knowing:
- The source filter has no Connector option. Connector rows carry a
CONNECTORbadge but are only visible while the source filter is on All sources — picking Bash or HTTP hides them. - The status filter has no Skipped option. Skipped runs are matched by Errors.
The KPI dropdown is built from your current KPIs, so a KPI you have deleted has rows in the log but no entry in the filter. You will find them under All KPIs.
With nothing logged at all the list reads “No fetch log entries yet.” When filters exclude everything, it reads “No entries match this filter.”
There is no free-text search, no date range, no paging and no sorting control.
How many entries are kept
The log is a ring buffer capped at 500 entries. A new entry is pushed on the front and the oldest one falls off the end. Nothing else prunes it — there is no age limit, so 500 entries can cover a week or a year depending on how many automatic KPIs you run and how often.
Rough arithmetic: five automatic KPIs on an hourly cron produce 120 rows a day and fill the buffer in about four days. The same five on a daily cron take three months.
Clearing the log
Clear sits at the top right of the screen and only appears when the log has at least one entry. It opens a native macOS dialog titled “Clear log” asking “Clear the entire fetch log?”. Confirm and the file is emptied.
Clearing cannot be undone, and the log is not part of any per-item backup. If you want to keep the evidence of a failure, take a JSON export first — see Backup, export & moving Macs.
Clearing has one side effect worth knowing. The scheduler decides whether a KPI is due by looking at its newest log entry; with the log empty it falls back to the KPI’s creation date instead. For a KPI that has existed for a while, that date is far in the past, so the next scheduler tick treats it as overdue and fetches it once. Expect a small burst of fresh rows within a minute of clearing. See Schedules & refresh.
Where the file lives
The log is one file, fetch-log.json, in your data folder. By default:
~/Library/Application Support/com.onepixelahead.pulsaria/pulsaria/fetch-log.json
If you have pointed Pulsaria at a different data folder, the log moves with it. It is a plain JSON array, newest first, and every field listed above is in there — including the ones the screen does not render.
The log is included in a JSON export but is deliberately never read back on import. Importing an export on a new Mac gives you your KPIs, entries, projects and connections, and an empty fetch log.
stdout, stderr and response bodies are stored verbatim. A command or an API that echoes a token or a customer name on failure puts that string into a file that also ends up in your exports. Keep secrets out of your output.
Debugging a broken source
The order that gets you to the answer fastest:
- Set the status filter to Errors and the KPI filter to the KPI in question.
- Look at the newest row. The message on the row is usually the whole story —
Exit code 1,HTTP 401,Output is not a number. - Expand the row. For Bash,
stderrtells you what the command complained about. For HTTP and connectors, thebodyblock holds the API’s own error response, which is almost always more specific than the status code. - Fix the cause — re-approve the command, renew the credential, correct the JSONPath.
- Press Run now on the KPI and come back. A fresh green row at the top means it is fixed.
There is no automatic retry and no backoff. A failed fetch simply waits for the next firing of its cron expression, and that failed attempt is what the scheduler uses to decide when “next” is — so a broken KPI is retried on schedule, not every minute.
Connector error messages
Connector failures get translated, human-readable messages. Detail: is followed by the first 200 characters of the API’s response body.
| Message | What to do |
|---|---|
Unauthorized (401). The access token was rejected. Detail: … | The credential is wrong, expired or revoked. Re-authorize the connection in App Settings → Connections |
Access denied (403). Make sure the Service Account email is granted access to the property. Detail: … | The credential is valid but has no access to that property or site. Grant it in Search Console or GA4 |
API not enabled in the Google Cloud project (403). Enable it and retry. Detail: … | Enable the Search Console or Analytics Data API in the Cloud project the key belongs to |
Bad request (400). Check parameters (site URL / property ID). Detail: … | A connection parameter is wrong — a site URL that does not match the verified property, a mistyped property ID |
Rate limit hit (429). Wait a minute and retry. | Nothing to fix. This is the only message without a detail snippet |
Server error (503). Try again later. Detail: … | The provider’s problem. The next scheduled run will pick it up |
Connection not found (deleted?) | The KPI points at a connection that no longer exists. Edit the KPI and pick another |
Credentials missing from keychain. Please re-authorize the connection. | The Keychain item is gone. Re-authorize the connection |
Connector KPI references no metric/connection | The KPI was saved without a metric or a connection. Edit it and finish the setup |
JSONPath "…" returned nothing | The API answered, but not in the expected shape. Read the body block |
More background on each connector is on the Connectors page.
Bash and HTTP error messages
| Message | Meaning |
|---|---|
Command not approved — please confirm in KPI settings | Skipped, not failed. Approve the command |
HTTP config not approved — please confirm in KPI settings | Skipped, not failed. Approve the HTTP config |
Command changed — please re-approve | The command was edited after approval. Test and approve it again |
HTTP config changed — please re-approve | The URL, headers or JSONPath changed after approval |
Exit code 1 | The command failed. Read stderr |
Output is not a number | The command ran but printed something Pulsaria cannot parse. Read stdout |
No command configured | The Bash KPI has an empty command field |
Shell commands are only available on macOS | Bash KPIs do not run on other platforms |
HTTP 404 | A custom HTTP KPI got a non-2xx status. Read the body block |
Response is not valid JSON | The endpoint answered with something other than JSON |
JSONPath result not numeric | The path matched, but the value is not a number |
URL or JSONPath missing | The HTTP KPI is incomplete |
Custom HTTP KPIs get the bare
HTTP <status>message — the friendly 401/403/400/429 wording above exists only on the connector path. For your own endpoints, thebodyblock in the expanded row is where the explanation is.
What this screen does not do
- No export of the log from here. It travels only inside a full JSON export.
- No search, no date filter, no paging.
- No live tail — the list refreshes when data changes, not continuously.
- No per-KPI retention or purge; Clear is all or nothing.
- Nothing in the log is sent anywhere. It is a file on your Mac, like the rest of your data — see Privacy: what leaves your Mac.