recapi 0.1.8 → 0.1.9

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/lib/recapi.rb +25 -7
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45a27aa9c5eae2fa2088d58247ebd703b60df834bc22e508737d89f17e90a8a2
4
- data.tar.gz: 7443f75901c5e2600ea181f3a61277bc9b2e5b2820aad2c999dba4b8353521b6
3
+ metadata.gz: 28f52d6426fd83d0b14d106535a06fca9e6d29ac56b4a3b366ae31f8ca7e22df
4
+ data.tar.gz: 93361e6a7d6dcbbc86be19b92ccc7fb4bc3b3695f99fa701ad08cfea17ccb57f
5
5
  SHA512:
6
- metadata.gz: 856de23aa6d4936301ac4d9a2dbd2de72c4ffea40e44ed41459f7b310ff36dc232103f9e8e6cd11d265790e6b95eda80cea2d6c40087d68df252d2861c5f2956
7
- data.tar.gz: dfd97019465b529c84fb4d315876acac76a414432fe3d53a901ec037f3b03c5490c2ce76ed2f3000651ff904f37115c9da9d046c59c99286924abcfea1ded8fc
6
+ metadata.gz: 54a7bf72a2b956ed847fe57af69c3241af18c638a199a86aa3220b2f0726d495402a3faa4698e011f815f57881e5969090bf9047b28b60bc0b6d11767f88eacf
7
+ data.tar.gz: a7825355e8be66451b687bc342a8c9961f8b7f8bc4c04fb7c57a38442ab59ed12daf67be7b04a39cba4b7a76843c5faec071e35dbcc15a5b3a1aba6f5154ae4b
data/README.md CHANGED
@@ -30,7 +30,7 @@ line with your address): save it as `lib/recapi.rb` and
30
30
  `require Rails.root.join("lib/recapi").to_s` instead.
31
31
 
32
32
  ```sh
33
- curl -fLo lib/recapi.rb https://api.<your-domain>/sdk/ruby/recapi-0.1.8.rb
33
+ curl -fLo lib/recapi.rb https://api.<your-domain>/sdk/ruby/recapi-0.1.9.rb
34
34
  ```
35
35
 
36
36
  ## Quick start
@@ -62,8 +62,9 @@ Ids may be strings or integers (an ActiveRecord id): they travel as strings.
62
62
 
63
63
  ## Calls
64
64
 
65
- A refused argument (an invalid id or level) raises `ArgumentError`, and an
66
- API failure `Recapi::Error` (`code`, `status`, `retry_after`, `retryable?`,
65
+ A refused argument (an invalid id or level) raises `ArgumentError`, except an
66
+ `item_id` or `context_item_id` the API would refuse, as a page often takes those
67
+ from a visitor's URL (see the table). An API failure is a `Recapi::Error` (`code`, `status`, `retry_after`, `retryable?`,
67
68
  `outage?`). `recommend` and `popular` still raise on a bad request (400, 404,
68
69
  413, other 3xx/4xx) or a reply that isn't the API's; an outage, a limit or a
69
70
  refused key can start on a live site with no deploy, so they answer with a list
@@ -71,10 +72,10 @@ and a log line.
71
72
 
72
73
  | Call | What it does |
73
74
  |---|---|
74
- | `track(item_id, level, user_id:, anon_id:, recommendation_id:)` | Records an interaction: `level` -10 to 10 or `:view` (3), `:cart` (5), `:purchase` (10), `:reject` (-10: never recommend the item to this user again until a positive event on it). The history changes at once; a background thread sends the event within 2 s (at once when a batch of 1,000 is full). |
75
+ | `track(item_id, level, user_id:, anon_id:, recommendation_id:)` | Records an interaction: `level` -10 to 10 or `:view` (3), `:cart` (5), `:purchase` (10), `:reject` (-10: never recommend the item to this user again until a positive event on it). The history changes at once; a background thread sends the event within 2 s (at once when a batch of 1,000 is full). An `item_id` the API would refuse (1-256 bytes of UTF-8, no control character; also `nil`, an Array or a Hash) drops the event with a log line: nothing raises, and the other events are sent as usual. A bad `level`, `user_id`, `anon_id` or `recommendation_id` still raises `ArgumentError`. |
75
76
  | `flush` | Sends the queued events now, with retries (end of a job). A network error, 429 or 5xx is retried 3 times (0.5 s, 1 s, 2 s with jitter, or the `Retry-After`, at most 8 s), then the events stay queued and the sender backs off up to a minute; any other error drops them (logged). |
76
77
  | `close` | Stops the sender and sends what is queued, one try per batch. It runs by itself at exit. |
77
- | `recommend(user_id:, anon_id:, count:, placement:, tags_any:, tags_all:, exclude:, context_item_id:, discovery:)` | A `Recapi::Recommendations` (`recommendation_id`, `fallback?`, `items`). At most 1.5 s; on a network error, timeout, 5xx or 429 it returns popular items (`fallback?`, no id), or no item when those fail too: hide the slot; on a 401 or 403 no item at once. Any other failure (a bad request, a reply that isn't the API's) raises. |
78
+ | `recommend(user_id:, anon_id:, count:, placement:, tags_any:, tags_all:, exclude:, context_item_id:, discovery:)` | A `Recapi::Recommendations` (`recommendation_id`, `fallback?`, `items`). At most 1.5 s; on a network error, timeout, 5xx or 429 it returns popular items (`fallback?`, no id), or no item when those fail too: hide the slot; on a 401 or 403 no item at once. A `context_item_id` the API would refuse (also an Array or a Hash, as `params[:id]` gives for `?id[]=a`) isn't sent: you get the same popular list and a log line, with no recommendation request. Any other failure (a bad request, a reply that isn't the API's) raises. |
78
79
  | `identify(anon_id, user_id)` | At login: the visitor's history and shown items become the user's. The user's first restore from recapi then leaves that visitor's browsing out, so it doesn't count twice. |
79
80
  | `popular(count:, tags_any:, tags_all:)` | Popular items, for pages without a user. On a network error, timeout, 5xx, 429, 401 or 403: an empty list and a log line. Other failures as in `recommend`. |
80
81
  | `upsert_items(items)` | Creates or replaces items (hashes of `item_id`, `title`, `tags`, `metadata`…), 1,000 per request, retried like events. Returns how many were stored. |
@@ -97,8 +98,8 @@ queue and sender at its first call. Proxies come from `HTTPS_PROXY`,
97
98
  `HTTP_PROXY` and `NO_PROXY`, as `Net::HTTP` reads them. Failures are logged
98
99
  to `logger:`, anything with `warn` and `error` (`Rails.logger`, a
99
100
  `Logger`); without one they go to `$stderr` as `recapi: …` lines, with lost
100
- events (a refused batch, rejected events) at most one line a minute for each
101
- error code and for rejected events, and `recommend`'s and `popular`'s failures
101
+ events (a refused batch, rejected events, a dropped `item_id`) at most one line a
102
+ minute for each error code, for rejected events and for dropped ids, and `recommend`'s and `popular`'s failures
102
103
  at most one line a minute for each error code, so a revoked key on a busy server
103
104
  doesn't flood the log, and a full queue one line until a send works. A failed write to
104
105
  `$stderr` (a full disk, a closed stream) is ignored. A logger you give gets
data/lib/recapi.rb CHANGED
@@ -25,7 +25,7 @@ rescue LoadError
25
25
  end
26
26
 
27
27
  module Recapi
28
- VERSION = "0.1.8"
28
+ VERSION = "0.1.9"
29
29
 
30
30
  # Decayed user history, exactly as docs/history-spec.md (times in unix ms).
31
31
  # A port of sdk/python's history.py, snapshot merging included.
@@ -487,14 +487,22 @@ module Recapi
487
487
  # is queued. level is an integer -10 to 10 or :view (3), :cart (5),
488
488
  # :purchase (10), :reject (-10: never recommend the item to this user
489
489
  # again until a positive event on it). recommendation_id overrides the
490
- # automatic attribution. Ids may be strings or integers.
490
+ # automatic attribution. Ids may be strings or integers. An item_id the API
491
+ # would refuse raises nothing: a page often takes it from a visitor's URL, so
492
+ # the event is dropped and logged instead.
491
493
  def track(item_id, level, user_id: nil, anon_id: nil, recommendation_id: nil)
492
494
  level = level_value(level)
493
- item_id = id_string(item_id)
494
- raise ArgumentError, "item_id #{ID_RULE}" unless History.id?(item_id)
495
-
496
495
  user_id, anon_id, recommendation_id = check_ids(user_id: user_id, anon_id: anon_id,
497
496
  recommendation_id: recommendation_id)
497
+ item_id = id_string(item_id)
498
+ # A crafted link must not fail the page. Nothing is kept: in the history it
499
+ # would make the API refuse every later list. The line never quotes the
500
+ # visitor's id.
501
+ unless History.id?(item_id)
502
+ log_lost(:item_id, "recapi: event dropped: item_id #{ID_RULE}")
503
+ return nil
504
+ end
505
+
498
506
  t = now_ms
499
507
  attributed = recommendation_id
500
508
  key = key_for(user_id, anon_id)
@@ -554,8 +562,11 @@ module Recapi
554
562
  # no id) and logs the error. If those fail too (recapi is down, or the plan's
555
563
  # limit covers them), the list is empty: hide the slot. On a 401 or 403 (a
556
564
  # revoked key, a suspended shop) it is empty at once, as popular would be
557
- # refused too. Any other failure raises: a bad request or a reply that isn't
558
- # the API's can't start on a live site without a deploy.
565
+ # refused too. A context_item_id the API would refuse (a page often takes it
566
+ # from a visitor's URL: Rails' params[:id] can be an Array or a Hash) is never
567
+ # sent: the answer is the same popular list. Any other failure raises: a bad
568
+ # request or a reply that isn't the API's can't start on a live site without
569
+ # a deploy.
559
570
  def recommend(user_id: nil, anon_id: nil, count: 0, placement: nil, tags_any: nil, tags_all: nil,
560
571
  exclude: nil, context_item_id: nil, discovery: nil)
561
572
  deadline = monotonic + recommend_timeout
@@ -565,6 +576,13 @@ module Recapi
565
576
  key = key_for(user_id, anon_id)
566
577
  t = now_ms
567
578
  state = key.nil? ? UserState.new : load(key)
579
+ unless context_item_id.nil? || context_item_id == "" || History.id?(context_item_id)
580
+ _, _, rejected = state.history.wire(t)
581
+ # The message never quotes the id: a visitor may have written it. nil:
582
+ # not skipped in the list either, as no item can have this id.
583
+ cause = Error.new("invalid_request", "context_item_id #{ID_RULE}")
584
+ return fallback(cause, count, with_rejected(exclude, rejected), nil, tags_any, tags_all)
585
+ end
568
586
  if user_id && !state.restored && !state.history.empty? && !@api_key.start_with?(PUBLIC_KEY_PREFIX)
569
587
  state = restore(key, user_id, state, deadline, t)
570
588
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - recapi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-11-04 00:00:00.000000000 Z
11
+ date: 2026-11-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'Ruby client for the recapi recommendation API: per-user history, event
14
14
  batching, retries, popular fallback. Standard library only.'