SafeSale · Blog

Who changed that price? Reading a sale's audit log

Every row in a SafeSale audit log: confirm, snapshot, apply, revert, errors, external edits, A/B results, who the actor is and what the log leaves out.

The question usually arrives on a Monday. A product that should be $49 is showing $39, or the compare-at price that should have come off after the weekend promotion is still there, and nobody in the team remembers touching it. Shopify tells you what the price is right now. It does not tell you what it was on Friday or who set it, and the product page has no timeline the way an order does. If a sale app was involved, the only honest record of what happened is the one the app kept.

This post is about that record in SafeSale: the audit log at the bottom of every sale page. It lists what each row means, which rows are written by the app on a schedule and which by a person, how to read a price change out of it, and what it does not capture, because knowing the gaps is as useful as knowing the entries.

Where it is and how to read it

Open any sale and scroll past the preview table and the A/B test section; the last section is titled Audit log. It is a table with three columns: when, action, and details. Entries are newest first and the page shows up to 200 of them. Each action is a small badge, shown in red for the two kinds of row that deserve attention, errors and external changes, and in grey for everything else. The details cell carries either a sentence written by the app or, for rows that changed a specific variant, a compact before-and-after: Variant 4471…: 49.00 → 39.00 (compare-at 49.00). After the sentence, in lighter text, is the actor.

The actor is the part people skim past and should not. It takes one of three values. merchant means a person clicked something in the SafeSale page: confirmed a preview, cancelled a scheduled sale, ended a live one, chose an A/B winner, or hit the store-wide restore. system means the app's own job runner did it at the scheduled time: took snapshots, wrote prices, created or deleted the automatic discount, restored prices. external means the change did not come from SafeSale at all and was detected after the fact. If you are trying to answer "did a person do this or did the schedule do this", the actor answers it in one word.

The rows in the order they usually appear

A sale that runs without incident produces a predictable sequence. Reading it once makes the exceptional cases obvious later.

  • confirm (merchant). Written when you confirm the preview. The message quotes the preview fingerprint, the number of variants that will be affected and the deepest discount in the sale: "Confirmed preview a3f9…: 212 variants, max 40% off". This is the row to point at when someone asks what was agreed; it is the exact set of prices that was on screen when the button was pressed.
  • snapshot (system). In the price rewrite mechanism, the first thing the start job does is store every variant's original price and compare-at price. One row summarises it: "Snapshotted original prices for 212 variants". If this row is missing, no price has been written yet.
  • apply (system). One row per variant written, with the before and after prices. These are the rows that answer "what did it change the price to", and on a large sale there are a lot of them.
  • discount create and discount delete (system). The automatic discount mechanism's equivalents: the Shopify discount was created at start and removed at end. No per-variant rows, because no variant price was touched; the difference between the two mechanisms is laid out in automatic discount versus compare-at price rewrite.
  • revert. Two flavours. "Restore original prices requested" with the actor merchant is a person ending the sale early, and is written the moment they click, before anything has been restored. The per-variant revert rows with the actor system are the restores themselves, one per variant, showing the price written back. A scheduled end produces only the second kind.
  • cancel (merchant). "Sale cancelled before it started". Only a draft or scheduled sale can be cancelled; a live sale is ended, which produces revert rows instead.

The rows that mean something went sideways

external change is the one this post opened with. A live sale has written $39 to a variant; Shopify's product update webhook arrives saying the variant is now $35, or that its compare-at price has gone. SafeSale compares the live values against what it wrote, and if they differ it logs a row with the actor external, the before (what SafeSale wrote) and the after (what is live now), and a fixed sentence: the price changed outside SafeSale while on sale, the original snapshot is unchanged and will be restored when the sale ends. It does not rewrite the live price, for the reasons in what happens when a price is edited during an active sale. What the row does not tell you is who made the external edit; the webhook does not carry a staff name, so neither can the log. It tells you when, to the second, which is usually enough to match against who was working.

error rows come from the job runner. A write to Shopify failed, the API rate-limited the app, a token was revoked. The message names the job and the attempt: "START_SALE attempt 2 failed, retrying: …" followed by the error text, and if the job exhausts its attempts, "gave up after N attempts". A sale with a retrying error row and then a run of apply rows recovered on its own; a sale with a gave-up row needs you, and the sale status will say failed. The reliability design behind the retries is in what happens when a discount app goes down on Black Friday.

experiment rows record the end of an A/B test: which variant won, by name and letter, and what happens next, either "now applies to everyone" or "ending the sale for everyone" if the no-discount bucket won. The actor is merchant, because concluding a test is always a click.

One more row is easy to miss. If prices moved between your preview and the scheduled start, the start job refuses to apply a stale plan and logs why, rather than writing discounts computed from numbers that are no longer true. Re-preview and confirm again, and the new confirm row carries the new fingerprint.

Answering the Monday question

Take the $39 product. Open the sale that included it; the audit log is per sale, so if you are not sure which one, the dashboard's list of recent sales narrows it. Scan for the variant's apply row to see what SafeSale wrote and when. Then look for any external change row on the same variant after it: if there is one, a person or another tool moved the price after the sale started, and the after value in that row is what they moved it to. If there is none, and the sale has ended, look for the variant's revert row; if it is missing, the end job did not complete and there will be an error row explaining why.

If the price is wrong and the sale is over, the fix is on the same page: the snapshot still holds the original, and restore writes it back. If the sale is still running, leave the price alone and let the scheduled end restore it, or end the sale now. The snapshot approach, and why it is the only thing that makes a wrong price recoverable, is the subject of how sale apps leave wrong prices behind.

What the log does not record

It is worth being plain about the edges. The log identifies a person only as merchant; it does not store which Shopify staff account was signed in. If attribution to an individual matters to you, the lever is Shopify's staff permissions: limit who can open SafeSale, and the merchant rows belong to that short list. The log also only watches variants that are in a live sale. A price edited on a product that is not on sale leaves no trace here, because the webhook comparison only has something to compare against while a snapshot is applied.

Store-wide actions are recorded against the store rather than a sale. The "Restore ALL original prices" button on the dashboard, and the best-effort restore that runs when the app is uninstalled, both write rows, but you will not find them on any single sale's page. And the page shows the latest 200 rows, which a sale over a few hundred variants exceeds, since apply and revert are one row each per variant. For a complete per-variant record, use the snapshot CSV on the sale page: one line per variant with the original price, original compare-at, the applied values and the restore time. It is the same data the restore button reads, in a form your accountant can open.

None of this is exotic. It is a list of what happened, with a clock and a one-word actor, kept next to the thing it describes. Most of the time nobody reads it. On the Monday that somebody needs to, SafeSale has already written it down.

Frequently asked questions

Does Shopify keep a history of price changes on a product?

Not one you can open on the product page. The admin shows the current price and compare-at price; it does not list earlier values or the staff member who set them. SafeSale's audit log covers the prices it touches: for every variant in a sale it records the price before, the price written, and the restore, with a timestamp. Prices edited by hand outside a sale are only recorded while that variant is in a live sale, as an external change.

Can I see which staff member confirmed or ended a sale?

The log distinguishes three actors: merchant for actions taken in the SafeSale admin page, system for the scheduled jobs that snapshot, write and restore prices, and external for edits detected through Shopify's product webhook. It does not record which staff account was signed in, so if you need that level of attribution, restrict who can open the app in Shopify's staff permissions and let the merchant rows map to those people.

How far back does the audit log go?

Rows are kept with the sale and are not pruned on a schedule; the sale page shows the most recent 200. A long sale on a big catalog can produce more rows than that, since every variant written and every variant restored is its own entry, so for a complete record of prices use the snapshot CSV export on the sale page, which lists each variant's original and applied prices.

Why does the log say a price changed outside SafeSale?

Someone or something edited a variant's price or compare-at price while the sale was live: a staff member on the product page, a bulk edit, a CSV import, or another app. SafeSale compares the live price against what it wrote, logs the difference with the actor external, and leaves the live price alone. The original snapshot is untouched and is what gets restored when the sale ends.