Data Lineage Example: Tracing Revenue From Source to Dashboard
Last updated June 2026 · Datatrail
Read-only connection. Datatrail never moves or mutates your data.
A concrete data lineage example for revenue looks like this: a dashboard's revenue tile reads fct_orders.revenue, which is summed from stg_stripe_charges.amount_usd, which is derived from the raw stripe.charges.amount column loaded by the Stripe connector. Tracing a metric from source to dashboard column by column is the clearest way to understand lineage, so this post walks the full chain for a single number, orders.revenue, and shows what you learn at each hop.
The scenario
An executive opens the morning dashboard and revenue looks 100x too high. The finance lead pings the data team. Without lineage, the team starts grepping SQL and guessing. With lineage, they pull up the chain behind that one tile and walk it backward. Here is the full path the tile depends on.
Hop 0: the dashboard tile
The revenue tile in the BI tool runs a query against the marts layer. It selects SUM(revenue) from fct_orders grouped by day. So the field we care about is fct_orders.revenue. This is the downstream-most node, and it sits past the dbt boundary in the BI tool, which is exactly the kind of consumer that dbt's own metadata does not know about. Lineage that includes downstream exposures picks this up by parsing the BI tool's queries from the warehouse query log.
Hop 1: the mart, fct_orders
Now we open fct_orders. Its SQL builds revenue from a join between order lines and charges. The relevant select expression is:
SUM(c.amount_usd) AS revenue
So fct_orders.revenue is a sum of amount_usd from an aliased staging model, joined on order_id. Column-level lineage records the edge stg_stripe_charges.amount_usd -> fct_orders.revenue, not just the table-level edge. That distinction matters here: fct_orders joins three staging models, but only one column feeds revenue, so we can ignore stg_customers and stg_products entirely. This is the payoff of column-level lineage: it narrows the search to the exact field instead of the whole table.
Hop 2: the staging model, stg_stripe_charges
Next we open stg_stripe_charges. Staging models do light cleanup: renaming, casting, and unit normalization. The revenue-relevant line is:
amount / 100.0 AS amount_usd
There it is. Stripe stores charge amounts in cents. The staging model divides by 100 to get dollars. So stg_stripe_charges.amount_usd = raw.stripe.charges.amount / 100. If someone recently removed that division, every dollar figure downstream would inflate by 100x, which is exactly the symptom on the dashboard. The lineage chain has already pointed us at the most likely culprit on the second hop.
Hop 3: the raw load, stripe.charges
The final hop is the raw table. stg_stripe_charges reads FROM raw.stripe.charges, the table the Stripe connector loads. The column amount is the original integer in cents, straight from the Stripe API. This is the source. There is nothing upstream of it inside the warehouse; the next step back would be the Stripe API itself.
The complete column-level chain for our number is:
raw.stripe.charges.amount(integer, cents, loaded hourly)stg_stripe_charges.amount_usd(= amount / 100)fct_orders.revenue(= SUM of amount_usd, joined on order_id)- Dashboard revenue tile (= SUM of fct_orders.revenue by day)
| Layer | Asset and column |
|---|---|
| Source | raw.stripe.charges.amount |
| Staging | stg_stripe_charges.amount_usd |
| Mart | fct_orders.revenue |
| Dashboard | revenue tile |
Finding the bug
Armed with the chain, the data team checks the recent git history on stg_stripe_charges. Sure enough, a refactor last night replaced amount / 100.0 with amount by mistake, dropping the division. Revenue inflated 100x exactly as predicted. The fix is one line. The diagnosis took minutes because lineage turned "revenue is wrong somewhere in the warehouse" into "check the one staging model that normalizes units."
What this would have looked like without lineage
Without lineage, the team would have to know offhand that fct_orders is the source of the tile, remember that it joins stg_stripe_charges, recall that Stripe amounts are in cents, and then go read the staging SQL to spot the missing division. On a warehouse with hundreds of models and several engineers, that institutional memory is fragile and often lives in one person's head. Lineage externalizes it into a graph anyone can read.
The same trace also tells you the blast radius. Because stg_stripe_charges.amount_usd feeds more than just fct_orders, the missing division did not only break the revenue tile. Any other model that sums amount_usd, such as a fct_payments mart or a daily revenue snapshot table, is wrong too. Lineage surfaces that full set of affected assets in one view, so the team patches the staging model once and knows exactly which downstream tables to rebuild and which dashboards to re-verify, rather than discovering a second broken chart two days later.
Adding freshness to the trace
The chain also encodes timing expectations. Hop 3 noted that raw.stripe.charges loads hourly. That single fact attaches a freshness SLA to the whole revenue chain: if the raw load is six hours late, every node downstream is serving stale revenue, and the dashboard is wrong for a different reason than the unit bug. Pairing the lineage chain with freshness monitoring means a late load on the source pages the team before anyone opens the dashboard, and the alert already names fct_orders and the revenue tile as the assets at risk. Lineage turns a generic late-load warning into a specific, prioritized incident.
Why trace-to-source needs to be automated
You could draw this chain by hand, and many teams do for their most important metrics. The problem is that the warehouse changes constantly. A new engineer adds a CTE, a column gets renamed, a mart gets split in two, and the hand-drawn diagram is wrong within a sprint. Automated lineage rebuilds the chain from what actually ran, by parsing the SQL in the warehouse query log and dbt metadata, so the trace you pull up reflects today's code, not last quarter's.
That parsing is also how the column-level edges get drawn. Snowflake's ACCESS_HISTORY resolves which columns a query read and wrote, and parsing the select lists of each model fills in the expression-level detail like the / 100.0 division. All of it is gathered read-only by design: lineage is reconstructed by reading query history and metadata, never by writing to your tables.
Trace your own revenue chain
The example above used orders.revenue, but the same walk applies to any field your stakeholders care about: customers.email, dim_users.signup_date, or a churn metric assembled from five models. If you want to click through your own warehouse and trace a real metric back to its raw source column, you can see how Datatrail maps dbt and warehouse lineage against a read-only connection and pull up the full chain behind any number on demand.
See how your data flows, end to end
Connect your warehouse read-only and map lineage, freshness, and downstream impact before a change breaks a dashboard. Transparent pricing, no card to start.