Class: ZeroClick::Sellers::Middleware::PageViews

Inherits:
Object
  • Object
show all
Defined in:
lib/zeroclick/sellers/middleware/page_views.rb

Overview

Rack middleware that emits a fire-and-forget page-view beacon for every page-content request, without changing the response. Mount it in front of the marketing site:

use ZeroClick::Sellers::Middleware::PageViews, seller: SELLER, seller_id: "sel_public"

The contract, shared by every ZeroClick seller SDK:

- only GET requests to non-asset paths are reported (an asset is a
final path segment ending in an extension — .css, .ico, .png);
- the app runs FIRST, so the beacon carries the real status and
duration; the visitor's user-agent, accept, and connecting IP are
forwarded for server-side classification;
- ANY beacon failure — missing key, network error, non-204, timeout,
a bad resolve_url or clock — is handed to +on_error+ and swallowed.
The middleware must never break or alter the customer's site.

Compose it as the OUTER middleware around Agentify — mount PageViews before Agentify — so the agent-markdown variant's re-exposed x-zcj-id is visible on the response and stitches the view onto the same journey.

Like Agentify it never reads the request body, so no buffering ceiling applies.

Constant Summary collapse

ASSET_PATH =

A page-content request has no asset extension on its final path segment. This skips /_astro/app.css, /favicon.ico, /logo.png and the like while still reporting /pricing, /blog/post, and / for both browsers and agents.

/\.[a-z0-9]+$/i
DEFAULT_CLOCK =

Wall clock in epoch milliseconds, for the observation time and the duration. Injectable so a test can make both deterministic.

-> { Process.clock_gettime(Process::CLOCK_REALTIME, :float_millisecond) }

Instance Method Summary collapse

Constructor Details

#initialize(app, seller_id:, seller: nil, clock: nil, resolve_url: nil, on_error: nil) ⇒ PageViews

seller may be a client, a callable returning one, or omitted to resolve the process-wide ZeroClick::Sellers.seller on the first request (the Rails path — see Base#initialize for why).

seller_id is the seller's public id, sent in every beacon (required by the wire contract). clock returns epoch milliseconds. resolve_url is a callable taking the Rack env and returning the public URL of the request, for servers behind a proxy that rewrites the host the runtime sees. on_error observes the fire-and-forget path; it is never re-raised.



56
57
58
59
60
61
62
63
# File 'lib/zeroclick/sellers/middleware/page_views.rb', line 56

def initialize(app, seller_id:, seller: nil, clock: nil, resolve_url: nil, on_error: nil)
  @app = app
  @seller_source = seller
  @seller_id = seller_id
  @clock = clock || DEFAULT_CLOCK
  @resolve_url = resolve_url
  @on_error = on_error
end

Instance Method Details

#call(env) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/zeroclick/sellers/middleware/page_views.rb', line 65

def call(env)
  started_at = @clock.call
  status, headers, body = @app.call(env)

  # The whole observation path is wrapped: a bad resolve_url, an
  # unparseable URL, a misbehaving clock, or the beacon delivery itself
  # must never break the page. Every failure — synchronous or from the
  # blocking beacon — is routed to on_error, and the app's response is
  # returned untouched regardless.
  begin
    observe(env, started_at: started_at, status: status, headers: headers)
  rescue StandardError => e
    @on_error&.call(e, env)
  end

  [status, headers, body]
end