pug-sh 0.0.1 → 0.0.2
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 +4 -4
- data/README.md +31 -23
- data/lib/pug/client.rb +16 -1
- data/lib/pug/event_builder.rb +27 -9
- data/lib/pug/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3fa18ab593cd55c417d83dc152eb219e7f7d3d5d6053a84abf426ace5028fde
|
|
4
|
+
data.tar.gz: 9cb0fc72b31991999476f98d1a49a65f707910b7f09020f405ef8fe1e7b81ca6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8076f0a24d015d3c1f40cc233cb77e880f913a5bebf0afa2bccbe7ad52acaaee57932c3e89cc85db2f79d146f2c35f0e5e1acf3285e35cb57a5c2e6b5cf8ceef
|
|
7
|
+
data.tar.gz: a89f4b45abb12e384855e49a142474dcf1c16a0b410fb5b96b6f0fc4a24c30b99388c6b4ac38221113753f641cd1c3392f3b30241871065e6276ae8b5e394412
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# pug-sh
|
|
6
6
|
|
|
7
|
-
Pug server-side SDK for Ruby
|
|
7
|
+
Pug server-side SDK for Ruby: event tracking, profile identify, and analytics reads.
|
|
8
8
|
|
|
9
9
|
It is instance-based and authenticates with a **private** project API key (`prv_…`). Ingestion
|
|
10
10
|
never raises, so a tracking call can't take down your request path.
|
|
@@ -25,13 +25,13 @@ require "pug"
|
|
|
25
25
|
|
|
26
26
|
pug = Pug::Client.new(api_key: ENV.fetch("PUG_API_KEY"))
|
|
27
27
|
|
|
28
|
-
# Track an event. distinct_id (who the event is for) comes first
|
|
28
|
+
# Track an event. distinct_id (who the event is for) comes first, as a server has no ambient user.
|
|
29
29
|
pug.track("user_42", "order.completed", amount: 49.0, currency: "USD")
|
|
30
30
|
|
|
31
31
|
# Well-known events get their properties type-checked and validated against the event's schema.
|
|
32
32
|
pug.track("user_42", "feature_used", feature_id: "export")
|
|
33
33
|
|
|
34
|
-
# Identify a profile (never raises
|
|
34
|
+
# Identify a profile (never raises; failures are logged).
|
|
35
35
|
pug.identify("user_42", email: "ada@example.com", plan: "pro")
|
|
36
36
|
|
|
37
37
|
# Reads raise Pug::Error.
|
|
@@ -56,9 +56,9 @@ pug.close
|
|
|
56
56
|
`max_wait` after the failure, but no sooner than a second; a permanent one (a fixed set of
|
|
57
57
|
client-error Connect codes, plus any non-transport error) dead-letters them via `on_error`.
|
|
58
58
|
`close` drains and reports anything still undelivered through `on_error` and the log.
|
|
59
|
-
`on_error` covers buffered `track` events only
|
|
59
|
+
`on_error` covers buffered `track` events only: `identify` failures are logged, and so are
|
|
60
60
|
events the server drops from a batch it accepts (`BatchCreateResponse.dropped`), at WARN.
|
|
61
|
-
- **Reads are request/response and do raise** `Pug::Error
|
|
61
|
+
- **Reads are request/response and do raise** `Pug::Error`: they run in your control flow, so
|
|
62
62
|
you own retry. They are not auto-retried.
|
|
63
63
|
- **Well-known events** have typed, validated properties; any other `kind` is accepted as a
|
|
64
64
|
custom event with loose properties.
|
|
@@ -72,7 +72,7 @@ pug.close
|
|
|
72
72
|
|
|
73
73
|
| Option | Type | Default | Description |
|
|
74
74
|
| ---------- | ------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
-
| `api_key` | `String` |
|
|
75
|
+
| `api_key` | `String` | none | **Required.** Private project key (`prv_…`). |
|
|
76
76
|
| `endpoint` | `String` | hosted endpoint | Pug server origin. |
|
|
77
77
|
| `batch` | `Hash` | see below | Ingestion buffer: `max_size` (100, capped at 1000), `max_wait` (5 seconds), `max_queue_size` (10,000). |
|
|
78
78
|
| `on_error` | callable | none | Dead-letter hook for undeliverable buffered `track` events, called with `(error, events)`. |
|
|
@@ -100,13 +100,13 @@ they behave as anywhere else, blocking the thread that called `track`.
|
|
|
100
100
|
|
|
101
101
|
### Ingestion (never raises)
|
|
102
102
|
|
|
103
|
-
- `pug.track(distinct_id, kind, properties = nil, options = nil)
|
|
104
|
-
- `pug.identify(external_id, traits = nil, options = nil)
|
|
103
|
+
- `pug.track(distinct_id, kind, properties = nil, options = nil)`: enqueue an event. Non-blocking.
|
|
104
|
+
- `pug.identify(external_id, traits = nil, options = nil)`: create or update a profile. Blocks
|
|
105
105
|
until the server answers.
|
|
106
|
-
- `pug.flush(timeout: nil)
|
|
106
|
+
- `pug.flush(timeout: nil)`: send the currently buffered events now and wait for that attempt.
|
|
107
107
|
Returns `false` if `timeout` (seconds) passed first. `true` means the attempt finished, not that
|
|
108
108
|
every event was delivered: a transient failure keeps them queued for a retry.
|
|
109
|
-
- `pug.close(timeout: nil)
|
|
109
|
+
- `pug.close(timeout: nil)`: drain and shut down; call on graceful exit so nothing buffered is lost.
|
|
110
110
|
Returns `false` if `timeout` passed first. Either way, by the time it returns every event it
|
|
111
111
|
couldn't send has gone to `on_error` and the log, including a batch still being sent when the
|
|
112
112
|
timeout ran out, which may yet arrive.
|
|
@@ -125,7 +125,7 @@ pug.track("user_42", "signup", { plan: "pro", location: "landing" }, location: {
|
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
`track` options: `timestamp` (a `Time`; defaults to now), `session_id` (a UUID overriding the
|
|
128
|
-
client's per-instance session), `location` (the visitor's location
|
|
128
|
+
client's per-instance session), `location` (the visitor's location; see below).
|
|
129
129
|
`identify` options, which work the same way with traits: `anonymous_id` (must start with `anon-`;
|
|
130
130
|
triggers anon→identified merge), `device_id`. Traits must be JSON-serializable, with nested values
|
|
131
131
|
mapped as for properties below; they travel as a protobuf `Struct`, whose numbers are doubles, so
|
|
@@ -150,14 +150,21 @@ becomes an ISO 8601 string and a `BigDecimal` or `Rational` a number, while a va
|
|
|
150
150
|
form for, such as a `Set`, drops the whole property with a warning. Strings are capped at 1024
|
|
151
151
|
bytes (truncated with a warning), and invalid UTF-8 is repaired.
|
|
152
152
|
|
|
153
|
+
Keys in snake_case are sent in camelCase, so `order_total:` arrives as `orderTotal`, the spelling
|
|
154
|
+
well-known event fields use and JavaScript code usually writes. Any other key, camelCase or not, is
|
|
155
|
+
sent as written, as are the keys inside a nested `Hash`. Pug's Node, Go and Python SDKs send every
|
|
156
|
+
key as written, so `order_total` from them stays `order_total`. If two keys come out the same, such
|
|
157
|
+
as `order_total` and `orderTotal`, the last one is sent and the other is dropped with a warning, as
|
|
158
|
+
with `Hash#merge`. Trait keys work the same way.
|
|
159
|
+
|
|
153
160
|
### Well-known events
|
|
154
161
|
|
|
155
162
|
For a well-known kind such as `purchase`, properties are checked against the event's schema.
|
|
156
163
|
Keys may be the field's name (`product_id`) or its JSON name (`productId`); either way the
|
|
157
|
-
property is sent under the JSON name, like every other Pug SDK
|
|
158
|
-
schema rather than the Ruby value, so `amount: 10` is
|
|
159
|
-
or one failing the schema's rules, drops the event
|
|
160
|
-
properties.
|
|
164
|
+
property is sent under the JSON name, like every other Pug SDK, but giving both drops the event
|
|
165
|
+
with an error. A field's type comes from the schema rather than the Ruby value, so `amount: 10` is
|
|
166
|
+
sent as a double. A value of the wrong type, or one failing the schema's rules, drops the event
|
|
167
|
+
with an error; other keys ride along as custom properties.
|
|
161
168
|
|
|
162
169
|
```ruby
|
|
163
170
|
pug.track("user_42", "purchase", product_id: "p1", amount: 49, currency: "USD", coupon: "FALL")
|
|
@@ -169,36 +176,37 @@ Duration fields take seconds as a number, `{ seconds:, nanos: }`, or a `Google::
|
|
|
169
176
|
### Location
|
|
170
177
|
|
|
171
178
|
Pug only derives geo from CDN headers on browser (public-key) requests. This SDK uses a private
|
|
172
|
-
key, so the server adds no geo of its own
|
|
179
|
+
key, so the server adds no geo of its own. Without `location`, an event has no geo at all:
|
|
173
180
|
|
|
174
181
|
```ruby
|
|
175
182
|
pug.track("user_42", "checkout_completed", { plan: "pro" }, location: { country: "DE", city: "Berlin" })
|
|
176
183
|
```
|
|
177
184
|
|
|
178
185
|
Fields: `continent`, `country`, `region`, `city`, `postal_code`, `metro_code`, `timezone`,
|
|
179
|
-
`latitude`, `longitude
|
|
186
|
+
`latitude`, `longitude`: strings, except the two coordinates. `country` must be an ISO 3166-1
|
|
180
187
|
alpha-2 code (case and surrounding spaces are normalized); `latitude` and `longitude` are only
|
|
181
188
|
sent as a pair.
|
|
182
189
|
|
|
183
|
-
A field that is missing, nil or empty is omitted silently. One that looks like a mistake
|
|
184
|
-
unknown key, wrong type, unknown country, coordinate out of range
|
|
190
|
+
A field that is missing, nil or empty is omitted silently. One that looks like a mistake
|
|
191
|
+
(unknown key, wrong type, unknown country, coordinate out of range) is dropped with a warning and
|
|
185
192
|
the rest is still sent. If nothing is usable the event goes out with no geo.
|
|
186
193
|
|
|
187
194
|
### Reads (raise `Pug::Error`)
|
|
188
195
|
|
|
189
196
|
These run in your control flow:
|
|
190
197
|
|
|
191
|
-
- `pug.profiles
|
|
198
|
+
- `pug.profiles`: `get(id, include_bots: false)`, `get_by_external_id(external_id, include_bots: false)`,
|
|
192
199
|
`delete(id)`, `list(request = nil, timeout: nil)` (auto-paginating: yields each profile to a
|
|
193
200
|
block, or returns an `Enumerator`). The timeout bounds the whole stream, the time your block
|
|
194
201
|
takes included, so `timeout:` (seconds) can give one long `list` more than the client's.
|
|
195
|
-
- `pug.activity
|
|
202
|
+
- `pug.activity`: `feed`, `event_explorer`, `heatmap`, `profile_sessions`, `profile_stats`,
|
|
196
203
|
`filter_schema`, `property_values`.
|
|
197
|
-
- `pug.insights
|
|
204
|
+
- `pug.insights`: `query`, `segment_users`, `filter_schema`, `property_values`.
|
|
198
205
|
|
|
199
206
|
Requests are a Hash or the generated request message, and responses are the generated messages,
|
|
200
207
|
under `Pug::Proto` (`Pug::Proto::Shared::Insights::V1::QueryResponse`, …); call `to_h` on one
|
|
201
|
-
for plain Ruby data.
|
|
208
|
+
for plain Ruby data. Property names are the ones stored, so a property tracked as `order_total:`
|
|
209
|
+
is filtered on, and comes back, as `orderTotal`.
|
|
202
210
|
|
|
203
211
|
```ruby
|
|
204
212
|
sessions = pug.activity.profile_sessions(distinct_id: "user_42", page_size: 50)
|
data/lib/pug/client.rb
CHANGED
|
@@ -239,10 +239,25 @@ module Pug
|
|
|
239
239
|
# Struct can't carry, such as NaN. A Struct's numbers are doubles, so an integer past 2^53
|
|
240
240
|
# would arrive changed.
|
|
241
241
|
def to_struct(traits)
|
|
242
|
-
ready = EventBuilder.json_ready(traits, max_integer: EventBuilder::MAX_SAFE_FLOAT)
|
|
242
|
+
ready = EventBuilder.json_ready(camel_case_keys(traits), max_integer: EventBuilder::MAX_SAFE_FLOAT)
|
|
243
243
|
::Google::Protobuf::Struct.from_hash(JSON.parse(JSON.generate(ready)))
|
|
244
244
|
rescue StandardError => e
|
|
245
245
|
raise NotSerializable, e.message
|
|
246
246
|
end
|
|
247
|
+
|
|
248
|
+
# Only the top level, as for properties: a nested Hash is as likely data keyed by ids.
|
|
249
|
+
def camel_case_keys(traits)
|
|
250
|
+
given = {}
|
|
251
|
+
traits.each_with_object({}) do |(key, value), out|
|
|
252
|
+
name = key.to_s
|
|
253
|
+
name = EventBuilder.camel_case(name.encoding.ascii_compatible? ? name : name.encode(Encoding::UTF_8))
|
|
254
|
+
if given.key?(name)
|
|
255
|
+
@log.warn("identify() traits #{given[name].inspect} and #{key.inspect} both go out as #{name.inspect}; " \
|
|
256
|
+
"sending the last")
|
|
257
|
+
end
|
|
258
|
+
given[name] = key
|
|
259
|
+
out[name] = value
|
|
260
|
+
end
|
|
261
|
+
end
|
|
247
262
|
end
|
|
248
263
|
end
|
data/lib/pug/event_builder.rb
CHANGED
|
@@ -30,6 +30,8 @@ module Pug
|
|
|
30
30
|
# Past 2^53 a Float may already have been rounded, so it can't stand for an exact integer.
|
|
31
31
|
MAX_SAFE_FLOAT = 2**53
|
|
32
32
|
|
|
33
|
+
SNAKE_CASE = /\A[a-z][a-z0-9]*(?:_[a-z0-9]+)+\z/
|
|
34
|
+
|
|
33
35
|
INTEGER_RANGES = {
|
|
34
36
|
int32: (-(2**31))..((2**31) - 1),
|
|
35
37
|
sint32: (-(2**31))..((2**31) - 1),
|
|
@@ -66,15 +68,15 @@ module Pug
|
|
|
66
68
|
Timestamp = ::Google::Protobuf::Timestamp
|
|
67
69
|
|
|
68
70
|
SchemaField = Struct.new(:descriptor, :name, :json_name, :type)
|
|
69
|
-
Schema = Struct.new(:klass, :name, :
|
|
71
|
+
Schema = Struct.new(:klass, :name, :by_json_name, :fields)
|
|
70
72
|
|
|
71
|
-
# Field lookups for every well-known schema, by
|
|
73
|
+
# Field lookups for every well-known schema, by JSON name, worked out once.
|
|
72
74
|
SCHEMAS = WellKnown::CATALOG.transform_values do |klass|
|
|
73
75
|
fields = klass.descriptor.map do |field|
|
|
74
76
|
SchemaField.new(field, field.name, field.json_name, field.type).freeze
|
|
75
77
|
end
|
|
76
|
-
|
|
77
|
-
Schema.new(klass, klass.descriptor.name,
|
|
78
|
+
by_json_name = fields.to_h { |field| [field.json_name, field] }
|
|
79
|
+
Schema.new(klass, klass.descriptor.name, by_json_name.freeze, fields.freeze).freeze
|
|
78
80
|
end.freeze
|
|
79
81
|
|
|
80
82
|
class FieldError < StandardError
|
|
@@ -127,6 +129,13 @@ module Pug
|
|
|
127
129
|
end
|
|
128
130
|
end
|
|
129
131
|
|
|
132
|
+
# Protobuf's JSON-name rule, so order_total lands on a well-known field's JSON name. Only
|
|
133
|
+
# lowercase snake_case is renamed, since the full rule would turn ORDER_ID into ORDERID; the
|
|
134
|
+
# ascii_only? check keeps match? from raising on a UTF-16 or broken key.
|
|
135
|
+
def self.camel_case(key)
|
|
136
|
+
key.ascii_only? && key.match?(SNAKE_CASE) ? key.gsub(/_[a-z0-9]/) { |pair| pair[1].upcase } : key
|
|
137
|
+
end
|
|
138
|
+
|
|
130
139
|
def initialize(log)
|
|
131
140
|
@log = log
|
|
132
141
|
end
|
|
@@ -213,8 +222,8 @@ module Pug
|
|
|
213
222
|
end
|
|
214
223
|
|
|
215
224
|
# Typed fields render by their proto type rather than their Ruby value, so a whole-number
|
|
216
|
-
# double stays a double.
|
|
217
|
-
# ride along as custom properties.
|
|
225
|
+
# double stays a double. A key names a field once camel-cased, so the proto name works as well
|
|
226
|
+
# as the JSON name; unknown keys ride along as custom properties.
|
|
218
227
|
def well_known_properties(schema, kind, properties)
|
|
219
228
|
message, extras = typed_message(schema, properties)
|
|
220
229
|
violations = Validator.validate(message)
|
|
@@ -234,9 +243,9 @@ module Pug
|
|
|
234
243
|
extras = {}
|
|
235
244
|
properties.each do |key, value|
|
|
236
245
|
name = utf8(key.to_s)
|
|
237
|
-
field = schema.
|
|
246
|
+
field = schema.by_json_name[self.class.camel_case(name)]
|
|
238
247
|
if field.nil?
|
|
239
|
-
extras[
|
|
248
|
+
extras[key] = value
|
|
240
249
|
elsif given.key?(field)
|
|
241
250
|
raise FieldError.new(field.name, "given twice, as #{given[field].first} and #{name}")
|
|
242
251
|
else
|
|
@@ -373,9 +382,18 @@ module Pug
|
|
|
373
382
|
end
|
|
374
383
|
|
|
375
384
|
def custom_properties(properties, kind)
|
|
385
|
+
given = {}
|
|
376
386
|
properties.each_with_object({}) do |(key, value), out|
|
|
387
|
+
name = self.class.camel_case(utf8(key.to_s))
|
|
377
388
|
mapped = custom_value(value)
|
|
378
|
-
|
|
389
|
+
next unless mapped
|
|
390
|
+
|
|
391
|
+
if given.key?(name)
|
|
392
|
+
@log.warn("Properties #{given[name].inspect} and #{key.inspect} on event \"#{kind}\" both go out as " \
|
|
393
|
+
"#{name.inspect}; sending the last")
|
|
394
|
+
end
|
|
395
|
+
given[name] = key
|
|
396
|
+
out[name] = mapped
|
|
379
397
|
rescue NotRepresentable => e
|
|
380
398
|
@log.warn("Property \"#{key}\" on event \"#{kind}\" not representable (#{e.message}), skipping")
|
|
381
399
|
end
|
data/lib/pug/version.rb
CHANGED