zeroclick-sellers 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2865ea4c52917887161bb00e22f46bdd275f28a6a7ac3a2e72d680b3db32323c
|
|
4
|
+
data.tar.gz: 4ca1a1c4665a099a54e15fb459a36eaa951feb718331fa6303cd4e81b2356d22
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b9b122f2cea8b056f79fe2aa56af4d674818148ae8e6b60adb4ac9b000520f06d360c22980c0c6900e87bc02882218d1e9e4815248b8d65d5319608e1a4d0a4
|
|
7
|
+
data.tar.gz: 81c0463cda57c90787550a9215a786d41d555486eaef5af19f081b965af84608d87819f2f053be1195425e815aa985d777761897090049bbced8dcb74981e73c
|
|
@@ -191,7 +191,9 @@ module ZeroClick
|
|
|
191
191
|
# +seller+ is the seller's public id; +path+ must be an absolute path.
|
|
192
192
|
def report_page_view(seller:, path:, status: nil, duration_ms: nil, representation: nil,
|
|
193
193
|
journey_id: nil, captured_at: nil, user_agent: nil, accept: nil,
|
|
194
|
-
client_ip: nil, country: nil, referrer_host: nil
|
|
194
|
+
client_ip: nil, country: nil, referrer_host: nil, host: nil,
|
|
195
|
+
eval_tag: nil, eval_signature: nil, eval_signature_input: nil,
|
|
196
|
+
eval_signature_agent: nil, source: nil, source_signature: nil)
|
|
195
197
|
if @page_views_key.nil? || @page_views_key.empty?
|
|
196
198
|
raise Error.new("page_views_not_configured", operation: "report_page_view")
|
|
197
199
|
end
|
|
@@ -200,7 +202,10 @@ module ZeroClick
|
|
|
200
202
|
seller: seller, path: path, status: status, duration_ms: duration_ms,
|
|
201
203
|
representation: representation, journey_id: journey_id, captured_at: captured_at,
|
|
202
204
|
user_agent: user_agent, accept: accept, client_ip: client_ip,
|
|
203
|
-
country: country, referrer_host: referrer_host
|
|
205
|
+
country: country, referrer_host: referrer_host, host: host,
|
|
206
|
+
eval_tag: eval_tag, eval_signature: eval_signature,
|
|
207
|
+
eval_signature_input: eval_signature_input, eval_signature_agent: eval_signature_agent,
|
|
208
|
+
source: source, source_signature: source_signature
|
|
204
209
|
)
|
|
205
210
|
status_code, body = @http_post.call(
|
|
206
211
|
base_url: @api_base_url,
|
|
@@ -22,6 +22,14 @@ module ZeroClick
|
|
|
22
22
|
# - the app runs FIRST, so the beacon carries the real status and
|
|
23
23
|
# duration; the visitor's user-agent, accept, and connecting IP are
|
|
24
24
|
# forwarded for server-side classification;
|
|
25
|
+
# - the visitor's country rides along when the edge resolved it: the
|
|
26
|
+
# header Cloudflare, CloudFront, Vercel or App Engine sets, or
|
|
27
|
+
# +resolve_country+; a host whose edge sets none sends no country;
|
|
28
|
+
# - a ZeroClick agent-eval request's tag and signature are forwarded
|
|
29
|
+
# with the page's host, so the API can keep ZeroClick's own eval
|
|
30
|
+
# traffic out of the seller's numbers; a browser-based run, which
|
|
31
|
+
# cannot sign, is told by the +src+ marker on its start URL,
|
|
32
|
+
# forwarded with the host and the signature beside it;
|
|
25
33
|
# - ANY beacon failure — missing key, network error, non-204, timeout,
|
|
26
34
|
# a bad resolve_url or clock — is handed to +on_error+ and swallowed.
|
|
27
35
|
# The middleware must never break or alter the customer's site.
|
|
@@ -43,6 +51,27 @@ module ZeroClick
|
|
|
43
51
|
# duration. Injectable so a test can make both deterministic.
|
|
44
52
|
DEFAULT_CLOCK = -> { Process.clock_gettime(Process::CLOCK_REALTIME, :float_millisecond) }
|
|
45
53
|
|
|
54
|
+
# ZeroClick's own start marker: the one query value the middleware
|
|
55
|
+
# forwards. A browser-based eval run's requests leave from the agent
|
|
56
|
+
# vendor's cloud, out of reach of the signed tag, so agent-eval marks
|
|
57
|
+
# its start URL as ?src=ze-<experiment>-<n> instead, and the API keeps
|
|
58
|
+
# the whole visit out of the seller's numbers from it.
|
|
59
|
+
EVAL_SOURCE_PREFIX = "ze-"
|
|
60
|
+
|
|
61
|
+
# The vendor request headers (as Rack env keys) that carry the
|
|
62
|
+
# visitor's country as the edge resolved it, outermost layer first: a
|
|
63
|
+
# CDN in front of a platform sees the true client, while the platform
|
|
64
|
+
# behind it geolocates the CDN's edge, so the outer header wins when
|
|
65
|
+
# both are present. Each is the vendor's documented header, forwarded
|
|
66
|
+
# raw; the API clamps it to a 2-letter code and drops the vendors'
|
|
67
|
+
# unknown sentinels (Cloudflare XX, App Engine ZZ).
|
|
68
|
+
COUNTRY_HEADERS = %w[
|
|
69
|
+
HTTP_CF_IPCOUNTRY
|
|
70
|
+
HTTP_CLOUDFRONT_VIEWER_COUNTRY
|
|
71
|
+
HTTP_X_VERCEL_IP_COUNTRY
|
|
72
|
+
HTTP_X_APPENGINE_COUNTRY
|
|
73
|
+
].freeze
|
|
74
|
+
|
|
46
75
|
# +seller+ may be a client, a callable returning one, or omitted to
|
|
47
76
|
# resolve the process-wide ZeroClick::Sellers.seller on the first
|
|
48
77
|
# request (the Rails path — see Base#initialize for why).
|
|
@@ -51,15 +80,21 @@ module ZeroClick
|
|
|
51
80
|
# by the wire contract). +clock+ returns epoch milliseconds.
|
|
52
81
|
# +resolve_url+ is a callable taking the Rack env and returning the
|
|
53
82
|
# public URL of the request, for servers behind a proxy that rewrites
|
|
54
|
-
# the host the runtime sees
|
|
55
|
-
#
|
|
56
|
-
|
|
83
|
+
# the host the runtime sees; its path is reported and, with an eval
|
|
84
|
+
# tag, its host. +on_error+ observes the fire-and-forget
|
|
85
|
+
# path; it is never re-raised. +resolve_country+ is a callable taking
|
|
86
|
+
# the Rack env and returning the visitor's country, replacing the
|
|
87
|
+
# vendor-header read (COUNTRY_HEADERS) with the seller's own source:
|
|
88
|
+
# exactly one header, or a runtime that exposes the country another
|
|
89
|
+
# way. Return nil for unknown.
|
|
90
|
+
def initialize(app, seller_id:, seller: nil, clock: nil, resolve_url: nil, on_error: nil, resolve_country: nil)
|
|
57
91
|
@app = app
|
|
58
92
|
@seller_source = seller
|
|
59
93
|
@seller_id = seller_id
|
|
60
94
|
@clock = clock || DEFAULT_CLOCK
|
|
61
95
|
@resolve_url = resolve_url
|
|
62
96
|
@on_error = on_error
|
|
97
|
+
@resolve_country = resolve_country
|
|
63
98
|
end
|
|
64
99
|
|
|
65
100
|
def call(env)
|
|
@@ -86,7 +121,8 @@ module ZeroClick
|
|
|
86
121
|
return unless env["REQUEST_METHOD"] == "GET"
|
|
87
122
|
|
|
88
123
|
url = @resolve_url ? @resolve_url.call(env) : Agentify.public_url(env)
|
|
89
|
-
|
|
124
|
+
uri = URI.parse(url.to_s)
|
|
125
|
+
path = uri.path
|
|
90
126
|
path = "/" if path.nil? || path.empty?
|
|
91
127
|
return if ASSET_PATH.match?(path)
|
|
92
128
|
|
|
@@ -100,12 +136,69 @@ module ZeroClick
|
|
|
100
136
|
user_agent: env["HTTP_USER_AGENT"],
|
|
101
137
|
accept: env["HTTP_ACCEPT"],
|
|
102
138
|
client_ip: client_ip_of(env),
|
|
103
|
-
country: env
|
|
139
|
+
country: country_of(env),
|
|
104
140
|
referrer_host: referrer_host_of(env),
|
|
105
|
-
journey_id: journey_id_of(headers)
|
|
141
|
+
journey_id: journey_id_of(headers),
|
|
142
|
+
**start_marker_of(uri),
|
|
143
|
+
**eval_run_of(env, uri)
|
|
106
144
|
)
|
|
107
145
|
end
|
|
108
146
|
|
|
147
|
+
# ZeroClick's own agent-eval run, when the visitor carried its tag: the
|
|
148
|
+
# tag, the signature agent-eval puts on a seller's site (under
|
|
149
|
+
# ZeroClick-only header names, never Web Bot Auth's) and the public
|
|
150
|
+
# host it was signed for. Forwarded as sent; the API labels the view
|
|
151
|
+
# only when the signature verifies, so a forged tag labels nothing. A
|
|
152
|
+
# value outside what the beacon accepts (empty, or longer than
|
|
153
|
+
# agent-eval ever sends) is left out rather than sent, so a visitor's
|
|
154
|
+
# odd header can never cost the page view itself.
|
|
155
|
+
def eval_run_of(env, uri)
|
|
156
|
+
tag = capped(env["HTTP_X_ZEROCLICK_EVAL"], 512)
|
|
157
|
+
return {} if tag.nil?
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
host: capped(host_of(uri), 300),
|
|
161
|
+
eval_tag: tag,
|
|
162
|
+
eval_signature: capped(env["HTTP_X_ZEROCLICK_EVAL_SIGNATURE"], 1_024),
|
|
163
|
+
eval_signature_input: capped(env["HTTP_X_ZEROCLICK_EVAL_SIGNATURE_INPUT"], 1_024),
|
|
164
|
+
eval_signature_agent: capped(env["HTTP_X_ZEROCLICK_EVAL_SIGNATURE_AGENT"], 512)
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def capped(value, max)
|
|
169
|
+
value && !value.empty? && value.length <= max ? value : nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The public host the page was requested on, as the JS SDK's URL.host
|
|
173
|
+
# reads it: the host and any non-default port.
|
|
174
|
+
def host_of(uri)
|
|
175
|
+
return nil if uri.host.nil? || uri.host.empty?
|
|
176
|
+
|
|
177
|
+
uri.port && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# ZeroClick's own start marker, keyed like report_page_view: empty
|
|
181
|
+
# without one. The +src+ query value when it is ours, the page's host
|
|
182
|
+
# and the +signature+ agent-eval puts beside the marker, as sent: the
|
|
183
|
+
# only query values the beacon carries. Any other +src+ stays on the
|
|
184
|
+
# site, and a marker or signature past what the beacon accepts (or a
|
|
185
|
+
# query that does not decode) is left out rather than sent.
|
|
186
|
+
def start_marker_of(uri)
|
|
187
|
+
return {} if uri.query.nil? || uri.query.empty?
|
|
188
|
+
|
|
189
|
+
values = URI.decode_www_form(uri.query)
|
|
190
|
+
src = values.assoc("src")&.last
|
|
191
|
+
return {} unless src&.start_with?(EVAL_SOURCE_PREFIX) && src.length <= 32
|
|
192
|
+
|
|
193
|
+
{
|
|
194
|
+
host: capped(host_of(uri), 300),
|
|
195
|
+
source: src,
|
|
196
|
+
source_signature: capped(values.assoc("signature")&.last, 256)
|
|
197
|
+
}
|
|
198
|
+
rescue ArgumentError
|
|
199
|
+
{}
|
|
200
|
+
end
|
|
201
|
+
|
|
109
202
|
# What the app served for this page-content request, read off the
|
|
110
203
|
# response content-type so it reflects the real representation
|
|
111
204
|
# regardless of what produced it.
|
|
@@ -134,6 +227,20 @@ module ZeroClick
|
|
|
134
227
|
first.empty? ? nil : first
|
|
135
228
|
end
|
|
136
229
|
|
|
230
|
+
# The visitor's country as the edge resolved it: the first present of
|
|
231
|
+
# COUNTRY_HEADERS, or whatever +resolve_country+ returns. nil when the
|
|
232
|
+
# edge set none of them, so a host without one sends no country rather
|
|
233
|
+
# than a guess; an empty value is left out the same way.
|
|
234
|
+
def country_of(env)
|
|
235
|
+
value = if @resolve_country
|
|
236
|
+
@resolve_country.call(env)
|
|
237
|
+
else
|
|
238
|
+
COUNTRY_HEADERS.lazy.map { |key| env[key] }.find { |v| v && !v.strip.empty? }
|
|
239
|
+
end
|
|
240
|
+
value = value&.strip
|
|
241
|
+
value.nil? || value.empty? ? nil : value
|
|
242
|
+
end
|
|
243
|
+
|
|
137
244
|
# The referrer reduced to its host at the edge, so the full referrer
|
|
138
245
|
# (its path and query, which can carry identifying values) never leaves
|
|
139
246
|
# the site. A malformed referrer yields no host rather than the full URL.
|
|
@@ -146,11 +253,7 @@ module ZeroClick
|
|
|
146
253
|
rescue URI::InvalidURIError
|
|
147
254
|
nil
|
|
148
255
|
end
|
|
149
|
-
|
|
150
|
-
return nil if host.nil? || host.empty?
|
|
151
|
-
|
|
152
|
-
# Host and any non-default port only, matching the JS SDK's URL.host.
|
|
153
|
-
uri.port && uri.port != uri.default_port ? "#{host}:#{uri.port}" : host
|
|
256
|
+
uri && host_of(uri)
|
|
154
257
|
end
|
|
155
258
|
|
|
156
259
|
# Case-insensitive lookup over the app's response headers.
|
|
@@ -27,9 +27,15 @@ module ZeroClick
|
|
|
27
27
|
# Build the wire body: camelCase string keys, every absent optional key
|
|
28
28
|
# omitted. +seller+ is the seller's public id and +path+ must be an
|
|
29
29
|
# absolute path, both rejected before any request leaves the process.
|
|
30
|
+
# +host+ and the +eval_*+ values describe ZeroClick's own agent-eval run
|
|
31
|
+
# when the visitor carried its tag: the public host the run signed for
|
|
32
|
+
# and the tag with its signature, as sent. The API labels the view as
|
|
33
|
+
# eval traffic only when the signature verifies.
|
|
30
34
|
def build_payload(seller:, path:, status: nil, duration_ms: nil, representation: nil,
|
|
31
35
|
journey_id: nil, captured_at: nil, user_agent: nil, accept: nil,
|
|
32
|
-
client_ip: nil, country: nil, referrer_host: nil
|
|
36
|
+
client_ip: nil, country: nil, referrer_host: nil, host: nil,
|
|
37
|
+
eval_tag: nil, eval_signature: nil, eval_signature_input: nil,
|
|
38
|
+
eval_signature_agent: nil, source: nil, source_signature: nil)
|
|
33
39
|
Sellers.require_slug!(seller, "seller", "report_page_view")
|
|
34
40
|
unless path.is_a?(String) && path.start_with?("/")
|
|
35
41
|
raise Error.new("malformed_input", operation: "report_page_view",
|
|
@@ -51,6 +57,13 @@ module ZeroClick
|
|
|
51
57
|
payload["clientIp"] = client_ip unless client_ip.nil?
|
|
52
58
|
payload["country"] = country unless country.nil?
|
|
53
59
|
payload["referrerHost"] = referrer_host unless referrer_host.nil?
|
|
60
|
+
payload["host"] = host unless host.nil?
|
|
61
|
+
payload["evalTag"] = eval_tag unless eval_tag.nil?
|
|
62
|
+
payload["evalSignature"] = eval_signature unless eval_signature.nil?
|
|
63
|
+
payload["evalSignatureInput"] = eval_signature_input unless eval_signature_input.nil?
|
|
64
|
+
payload["evalSignatureAgent"] = eval_signature_agent unless eval_signature_agent.nil?
|
|
65
|
+
payload["source"] = source unless source.nil?
|
|
66
|
+
payload["sourceSignature"] = source_signature unless source_signature.nil?
|
|
54
67
|
payload
|
|
55
68
|
end
|
|
56
69
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zeroclick-sellers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ZeroClick
|
|
@@ -15,7 +15,7 @@ description: |
|
|
|
15
15
|
and report what was used. Ships Rack middleware, so it works in Rails,
|
|
16
16
|
Sinatra, Hanami and Roda alike.
|
|
17
17
|
email:
|
|
18
|
-
-
|
|
18
|
+
- help@zeroclick.ai
|
|
19
19
|
executables: []
|
|
20
20
|
extensions: []
|
|
21
21
|
extra_rdoc_files: []
|