where_is_waldo 0.1.9 → 0.1.11

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: 96f9fb4253fa91bfaa947711aff7e9de6cf6511029c34fe4d24bcaa92be9958d
4
- data.tar.gz: f441dacf17ca081d1974d08e3eb3460dce8ca5423772689c72e730c58746b8e9
3
+ metadata.gz: 5bd28c86d109c69fc976eaab788972106493a78b3fbd8e8e2370bae5dc936986
4
+ data.tar.gz: fd49e94bf3a08dd4350fdc1b8b413ec5d66916ef1ada8c74162044b3ccbdc428
5
5
  SHA512:
6
- metadata.gz: df11e85496b2ae55c928695e1dad936a82b0d0b3abbcf8df7160932cf221f0ab4ab7c1cb8830cc5b710bbbab029cb82dfdb6d719292b0dc1863767151baf406c
7
- data.tar.gz: 3412d9fd5475df5fe17a29c8dbd910f3c920db8260971492aa479ab23797d98d0c726549328aa27bdbef675646ae750f948c30ac810c96eaadddf6d8e1badcdf
6
+ metadata.gz: 620c8ce6c899937a25ab39f2858e45c5fa6ac68dc3510cf0e47a5739b36a8f95c24bb3daf385974600ffa652ad32f176a79ac0389286520af7a291bc48cda1e7
7
+ data.tar.gz: 0ecf65ed538cbffc439428ed3f5acc690e178430ba62a9568fb1b4a1d4cb48f33303aaa9724979f8c1c0ebfc44da5eb35133665345a2dfc2d4a3a224373784d7
data/CHANGELOG.md CHANGED
@@ -3,6 +3,93 @@
3
3
  Notable changes to where_is_waldo. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
+ ## Unreleased
7
+
8
+ ## 0.1.11 - 2026-10-27
9
+
10
+ ### Fixed
11
+
12
+ - **The install generator emitted the pre-0.1.5 schema.** It wrote a unique
13
+ index on the session column alone, while 0.1.5 moved presence rows to being
14
+ keyed by `(subject, session)` precisely so two authenticated subjects who
15
+ collide on a session id cannot upsert onto each other's row — session ids are
16
+ client-supplied in some setups, and a reused JWT `jti` is enough. The adapter
17
+ has scoped every session lookup by both columns since; only the generator
18
+ lagged, so every app installed after 0.1.5 got a schema the library had
19
+ already moved away from and needed a follow-up migration to correct. An
20
+ unused second migration template carrying the right index (and a hardcoded
21
+ `Migration[7.0]`) has been removed rather than left as a second answer.
22
+
23
+ - **The install generator overwrote an existing ActionCable connection.**
24
+ Installing into an app that already authenticates its own cable connection
25
+ replaced that file with the turnkey one, silently dropping whatever the app
26
+ did beyond what this library needs — container scoping and token bookkeeping
27
+ in the cases seen. Existing channel files are now kept, and the post-install
28
+ message names them and states what the channel requires of a connection
29
+ (`current_user` and `session_id`).
30
+
31
+ ### Changed
32
+
33
+ - **The Redis adapter is now tested against a real server, on both redis-rb 5
34
+ and 6.** Its existing spec runs against `MockRedis`, a pure-Ruby fake with no
35
+ protocol layer, so it could not see wire-format changes at all. redis-rb 6
36
+ defaults to RESP3, which types several replies differently from RESP2 —
37
+ `SMEMBERS` among them, and this adapter feeds that result straight into
38
+ `filter_map` and `all?`. Nothing in the adapter's command set behaves
39
+ differently between the two, but that was previously unverified rather than
40
+ known.
41
+
42
+ - New integration spec drives the adapter against a live Redis. It skips when
43
+ no server is reachable, so contributors are not required to run one.
44
+ - CI adds an `rspec-redis` job with a Redis service, run as a matrix over
45
+ redis-rb 5.x and 6.x, and fails rather than skips if the service is missing
46
+ — a silent skip would read as a pass.
47
+ - Development dependency widened from `~> 5.0` to `>= 5.0, < 7`. The gem has
48
+ no runtime dependency on `redis` and never has, so this constrains nothing
49
+ for consumers; it only lets the library test what consumers actually use.
50
+ - README documents supported client versions, and that `protocol: 2` is not
51
+ needed.
52
+
53
+ The committed lockfile stays on redis 5.x, which keeps `mock_redis` at
54
+ 0.53.0 for every existing spec. `mock_redis` 0.53.0 depends on `redis ~> 5`,
55
+ so resolving against redis 6 forces it down to 0.48.1 — that happens only
56
+ inside the redis 6 CI leg, which runs the real-server spec alone and never
57
+ touches MockRedis. No mock-based spec changed the double it runs against.
58
+
59
+ No runtime code changed.
60
+
61
+ ## 0.1.10
62
+
63
+ ### Added
64
+
65
+ - **Presence heartbeats are kept out of APM as their own transactions.**
66
+ `WhereIsWaldo::PresenceChannel#heartbeat` now runs inside
67
+ `WhereIsWaldo::Apm.ignoring_transaction`, so when a host app runs New Relic
68
+ the per-tab-per-interval heartbeat — the highest-volume, least-interesting
69
+ cable action — no longer surfaces as an APM transaction. Left unhandled it
70
+ inflates throughput and flatters average response time (heartbeats are
71
+ uniformly fast) while telling operators nothing the presence store can't
72
+ already answer.
73
+
74
+ - Guarded, no new dependency: `Apm` no-ops unless a supported agent
75
+ (New Relic today) is loaded, so consumers without APM pay nothing.
76
+ - Failures stay visible: an ignored New Relic transaction skips `commit!`,
77
+ where exceptions are normally harvested, so the wrapper hands any heartbeat
78
+ exception straight to the error collector and re-raises. Ignoring the
79
+ routine transaction never hides a heartbeat failure.
80
+ - Opt out with `config.ignore_heartbeat_apm = false` to report heartbeats
81
+ like any other action.
82
+ - Scope: New Relic only. The underlying exposure is agent-general — any APM
83
+ that opens a transaction per `perform_action` sees the same heartbeat
84
+ volume — but New Relic is the only agent seen in production so far.
85
+ Detection is isolated in `Apm.agent` so another vendor can be added there
86
+ without touching callers.
87
+
88
+ Note: the companion New Relic issue — the agent pinning the WebSocket upgrade
89
+ transaction onto pooled ActionCable worker threads — is a general
90
+ newrelic_rpm × ActionCable defect and stays a host-app shim; it is not
91
+ something the gem patches.
92
+
6
93
  ## 0.1.9
7
94
 
8
95
  Security/maintenance release — dependency bumps only, no library code changes.
data/README.md CHANGED
@@ -41,6 +41,13 @@ rails generate where_is_waldo:install --adapter=redis --subject_column=user_id
41
41
  npm install @byscott-io/where-is-waldo @rails/actioncable
42
42
  ```
43
43
 
44
+ Installing into an app that already has `app/channels/application_cable/`?
45
+ Those files are kept, never overwritten — the generator only adds what is
46
+ missing, and says which it left alone. The presence channel needs the
47
+ connection to identify **`current_user`** and **`session_id`**; add them to
48
+ your own connection, or `include WhereIsWaldo::JwtConnection` (corebyscott
49
+ apps) to get both from the JWT.
50
+
44
51
  ### 2. Configure
45
52
 
46
53
  ```ruby
@@ -113,6 +120,14 @@ WhereIsWaldo.configure do |config|
113
120
  config.timeout = 90 # seconds until offline
114
121
  config.heartbeat_interval = 30
115
122
 
123
+ # Observability: when true (default) and the host app runs a supported APM
124
+ # agent (New Relic today), presence heartbeats are kept out of APM as their
125
+ # own transactions. Heartbeats are the highest-volume, least-interesting cable
126
+ # action, so reporting each one inflates throughput and flatters average
127
+ # response time without adding signal. Failures are still reported. Set false
128
+ # to report heartbeats like any other action.
129
+ config.ignore_heartbeat_apm = true
130
+
116
131
  # Optional: custom subject data in presence hash. NOTE: with the roster
117
132
  # enabled, these fields are broadcast to every member of the org — see
118
133
  # Security.
@@ -140,6 +155,20 @@ WhereIsWaldo.configure do |config|
140
155
  end
141
156
  ```
142
157
 
158
+ #### Redis adapter and client versions
159
+
160
+ The gem does not depend on `redis`. Choosing `:redis` means supplying your own
161
+ client, so you pick the version.
162
+
163
+ `redis-rb` 5.x and 6.x are both supported and both run in CI against a real
164
+ server. Version 6 switched to RESP3 by default, which changes reply types for
165
+ some commands; none of the commands this adapter uses behave differently, and
166
+ the integration spec exists to keep that true. Nothing here needs `protocol: 2`.
167
+
168
+ The adapter stores presence under keys prefixed by `config.redis_prefix`
169
+ (default `where_is_waldo`) and sets a TTL on session keys, so it does not need a
170
+ cleanup job the way the database adapter does.
171
+
143
172
  ### Querying Presence
144
173
 
145
174
  ```ruby
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.9
1
+ 0.1.11
@@ -32,7 +32,20 @@ module WhereIsWaldo
32
32
  publish_roster_change
33
33
  end
34
34
 
35
+ # Kept out of APM as its own transaction by default — heartbeats are the
36
+ # highest-volume, least-interesting cable action, so reporting each one
37
+ # distorts throughput and average response time without adding signal. The
38
+ # wrapper no-ops unless the host runs a supported agent; see
39
+ # WhereIsWaldo::Apm and config.ignore_heartbeat_apm. The real work lives in
40
+ # perform_heartbeat, which is private so a client can't invoke it directly
41
+ # as a channel action (only public methods are processable actions).
35
42
  def heartbeat(data)
43
+ WhereIsWaldo::Apm.ignoring_transaction { perform_heartbeat(data) }
44
+ end
45
+
46
+ private
47
+
48
+ def perform_heartbeat(data)
36
49
  return if presence_suppressed?
37
50
 
38
51
  data = data.with_indifferent_access
@@ -58,8 +71,6 @@ module WhereIsWaldo
58
71
  publish_roster_change
59
72
  end
60
73
 
61
- private
62
-
63
74
  # Memoized per-subscription. Host apps configure suppress_presence_proc
64
75
  # to gate WHICH subscriptions register presence — e.g. a support-user
65
76
  # impersonation tab is a legitimate WS consumer but shouldn't count as
@@ -32,9 +32,17 @@ module WhereIsWaldo
32
32
  end
33
33
 
34
34
  def create_channels
35
- template "connection.rb.tt", "app/channels/application_cable/connection.rb"
36
- template "channel.rb.tt", "app/channels/application_cable/channel.rb"
37
- template "presence_channel.rb.tt", "app/channels/presence_channel.rb"
35
+ # NEVER overwrite an app's existing cable connection or channel base.
36
+ # A host app that already authenticates ActionCable has its auth,
37
+ # container scoping and session identity in those files, and the
38
+ # turnkey versions here provide only what this library needs -- so
39
+ # replacing them silently drops whatever else they did. Installing
40
+ # into an existing app is the common case, not the exception.
41
+ @kept_channels = []
42
+
43
+ copy_channel "connection.rb.tt", "app/channels/application_cable/connection.rb"
44
+ copy_channel "channel.rb.tt", "app/channels/application_cable/channel.rb"
45
+ copy_channel "presence_channel.rb.tt", "app/channels/presence_channel.rb"
38
46
  end
39
47
 
40
48
  def show_post_install_message
@@ -54,10 +62,34 @@ module WhereIsWaldo
54
62
  say " <App />"
55
63
  say " </PresenceProvider>"
56
64
  say ""
65
+ report_kept_channels
57
66
  end
58
67
 
59
68
  private
60
69
 
70
+ # Say plainly what was left alone. A silent skip is as confusing as a
71
+ # silent overwrite: the install looks complete and presence then fails
72
+ # because the app's own connection exposes no session identity.
73
+ def report_kept_channels
74
+ return if @kept_channels.blank?
75
+
76
+ say "Kept your existing files (not overwritten):", :yellow
77
+ @kept_channels.each { |f| say " - #{f}" }
78
+ say ""
79
+ say " WhereIsWaldo's channel reads connection.current_user and"
80
+ say " connection.session_id. If your connection does not identify"
81
+ say " both, add them -- or include WhereIsWaldo::JwtConnection."
82
+ say ""
83
+ end
84
+
85
+ def copy_channel(source, destination)
86
+ if File.exist?(File.join(destination_root, destination))
87
+ @kept_channels << destination
88
+ else
89
+ template source, destination
90
+ end
91
+ end
92
+
61
93
  def migration_version
62
94
  "[#{ActiveRecord::VERSION::STRING.to_f}]"
63
95
  end
@@ -14,7 +14,12 @@ class CreatePresences < ActiveRecord::Migration<%= migration_version %>
14
14
  t.timestamps
15
15
  end
16
16
 
17
- add_index :presences, :<%= session_column %>, unique: true
17
+ # (subject, session) must be unique TOGETHER, never session alone. Session
18
+ # ids are not globally unique -- they are client-supplied in some setups,
19
+ # and a reused JWT jti is enough -- so keying on the session alone lets two
20
+ # authenticated subjects upsert onto each other's presence row. This is the
21
+ # keying the adapter uses: every session lookup scopes by both columns.
22
+ add_index :presences, [ :<%= subject_column %>, :<%= session_column %> ], unique: true
18
23
  add_index :presences, :last_heartbeat
19
24
  end
20
25
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WhereIsWaldo
4
+ # APM (New Relic) integration seam.
5
+ #
6
+ # The gem takes NO hard dependency on newrelic_rpm. Every method here no-ops
7
+ # unless a supported agent is loaded in the host app, so a consumer without
8
+ # New Relic pays nothing and nothing breaks.
9
+ #
10
+ # Why this exists: ActionCable dispatches each client action through New
11
+ # Relic's ActionCableSubscriber, which starts one APM transaction per
12
+ # `perform_action`. Presence heartbeats are the highest-volume, least
13
+ # interesting action Waldo has — one per open tab per heartbeat interval,
14
+ # uniformly fast — so left alone they dominate transaction throughput and
15
+ # flatten the host app's average response time while telling operators nothing
16
+ # the presence store can't already answer. `ignoring_transaction` keeps them
17
+ # out of APM's headline numbers without hiding failures.
18
+ #
19
+ # (The companion half of this problem — the agent pinning the WebSocket
20
+ # upgrade transaction onto pooled ActionCable worker threads — is a general
21
+ # newrelic_rpm x ActionCable defect, not Waldo's to fix here. Host apps carry
22
+ # that shim until the agent resets pooled-thread state itself.)
23
+ #
24
+ # Scope: this handles New Relic ONLY. The underlying exposure is agent-general
25
+ # — any APM/tracer that opens a transaction per `perform_action` (Datadog,
26
+ # Scout, AppSignal, Skylight, Sentry, OpenTelemetry) sees the same heartbeat
27
+ # flood, and several share the same pooled-thread context-leak. Waldo doesn't
28
+ # cause that (it spawns no threads and patches nothing global); its heartbeat
29
+ # volume amplifies whatever the host's agent already does. Detection is
30
+ # isolated in `agent` so another vendor can be added there without touching
31
+ # callers.
32
+ module Apm
33
+ module_function
34
+
35
+ # The New Relic agent module, or nil when newrelic_rpm isn't loaded (or is
36
+ # too old to expose the API we use). Isolated behind one method so specs can
37
+ # drive both the present and absent paths without defining a global
38
+ # ::NewRelic constant.
39
+ def agent
40
+ return nil unless defined?(::NewRelic::Agent)
41
+ return nil unless ::NewRelic::Agent.respond_to?(:ignore_transaction)
42
+
43
+ ::NewRelic::Agent
44
+ end
45
+
46
+ # Run a unit of work without letting it surface as its own APM transaction,
47
+ # and return the block's value.
48
+ #
49
+ # A plain `yield` when there's no agent, or when the host has opted out via
50
+ # `config.ignore_heartbeat_apm = false` — in either case the transaction is
51
+ # reported normally.
52
+ #
53
+ # New Relic's `Transaction#ignore!` makes `#finish` skip `commit!`, and
54
+ # `commit!` is where transaction-attached exceptions are harvested
55
+ # (`record_exceptions`). So ignoring on its own would ALSO swallow a
56
+ # heartbeat failure. We hand any exception straight to the error collector —
57
+ # which reports immediately rather than at commit — and re-raise, so the
58
+ # caller's own error handling is unchanged.
59
+ def ignoring_transaction
60
+ nr = agent
61
+ return yield unless nr && WhereIsWaldo.config.ignore_heartbeat_apm
62
+
63
+ nr.ignore_transaction
64
+
65
+ begin
66
+ yield
67
+ rescue StandardError => e
68
+ nr.instance&.error_collector&.notice_error(e)
69
+ raise
70
+ end
71
+ end
72
+ end
73
+ end
@@ -27,6 +27,15 @@ module WhereIsWaldo
27
27
  # :heartbeat_interval - expected heartbeat frequency (default: 30)
28
28
  attr_accessor :timeout, :heartbeat_interval
29
29
 
30
+ # Observability
31
+ # :ignore_heartbeat_apm - when true (default) and a supported APM agent
32
+ # (New Relic) is loaded in the host app, presence heartbeats are kept out
33
+ # of APM as their own transactions. Heartbeats are the highest-volume,
34
+ # least-interesting cable action, so reporting each one distorts
35
+ # throughput and average response time without adding signal. Set false
36
+ # to report them like any other action. See WhereIsWaldo::Apm.
37
+ attr_accessor :ignore_heartbeat_apm
38
+
30
39
  # ActionCable settings
31
40
  # :channel_name - defaults to 'WhereIsWaldo::PresenceChannel'
32
41
  # :authenticate_proc - proc to authenticate connection, receives request
@@ -120,6 +129,9 @@ module WhereIsWaldo
120
129
  @timeout = 90
121
130
  @heartbeat_interval = 30
122
131
 
132
+ # Observability defaults
133
+ @ignore_heartbeat_apm = true
134
+
123
135
  # ActionCable defaults
124
136
  @channel_name = "WhereIsWaldo::PresenceChannel"
125
137
  @authenticate_proc = nil
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "where_is_waldo/version"
4
4
  require "where_is_waldo/configuration"
5
+ require "where_is_waldo/apm"
5
6
  require "where_is_waldo/engine" if defined?(Rails)
6
7
 
7
8
  module WhereIsWaldo
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: where_is_waldo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Gibson
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-08-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -84,16 +83,22 @@ dependencies:
84
83
  name: redis
85
84
  requirement: !ruby/object:Gem::Requirement
86
85
  requirements:
87
- - - "~>"
86
+ - - ">="
88
87
  - !ruby/object:Gem::Version
89
88
  version: '5.0'
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '7'
90
92
  type: :development
91
93
  prerelease: false
92
94
  version_requirements: !ruby/object:Gem::Requirement
93
95
  requirements:
94
- - - "~>"
96
+ - - ">="
95
97
  - !ruby/object:Gem::Version
96
98
  version: '5.0'
99
+ - - "<"
100
+ - !ruby/object:Gem::Version
101
+ version: '7'
97
102
  - !ruby/object:Gem::Dependency
98
103
  name: rspec-rails
99
104
  requirement: !ruby/object:Gem::Requirement
@@ -210,11 +215,11 @@ files:
210
215
  - lib/generators/where_is_waldo/install/install_generator.rb
211
216
  - lib/generators/where_is_waldo/install/templates/channel.rb.tt
212
217
  - lib/generators/where_is_waldo/install/templates/connection.rb.tt
213
- - lib/generators/where_is_waldo/install/templates/create_presences.rb.tt
214
218
  - lib/generators/where_is_waldo/install/templates/initializer.rb.tt
215
219
  - lib/generators/where_is_waldo/install/templates/migration.rb.tt
216
220
  - lib/generators/where_is_waldo/install/templates/presence_channel.rb.tt
217
221
  - lib/where_is_waldo.rb
222
+ - lib/where_is_waldo/apm.rb
218
223
  - lib/where_is_waldo/configuration.rb
219
224
  - lib/where_is_waldo/engine.rb
220
225
  - lib/where_is_waldo/version.rb
@@ -227,7 +232,6 @@ metadata:
227
232
  changelog_uri: https://github.com/byscott-io/where_is_waldo/blob/master/CHANGELOG.md
228
233
  allowed_push_host: https://rubygems.org
229
234
  rubygems_mfa_required: 'true'
230
- post_install_message:
231
235
  rdoc_options: []
232
236
  require_paths:
233
237
  - lib
@@ -242,8 +246,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
246
  - !ruby/object:Gem::Version
243
247
  version: '0'
244
248
  requirements: []
245
- rubygems_version: 3.5.22
246
- signing_key:
249
+ rubygems_version: 4.0.16
247
250
  specification_version: 4
248
251
  summary: Real-time presence tracking for Rails + React applications
249
252
  test_files: []
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Create<%= table_name.camelize %> < ActiveRecord::Migration[7.0]
4
- def change
5
- create_table :<%= table_name %> do |t|
6
- # Identifiers - column names are configurable
7
- t.string :<%= session_column %>, null: false # Unique session/connection identifier
8
- t.bigint :<%= subject_column %>, null: false # Who (user, member, student, etc.)
9
-
10
- # Timestamps
11
- t.datetime :connected_at, null: false
12
- t.datetime :last_heartbeat, null: false
13
- t.datetime :last_activity
14
-
15
- # Activity state
16
- t.boolean :tab_visible, default: true, null: false
17
- t.boolean :subject_active, default: true, null: false
18
-
19
- # Extensible metadata
20
- t.<%= json_column_type %> :metadata
21
-
22
- t.timestamps
23
- end
24
-
25
- # (subject, session) must be unique together. Keying by session alone
26
- # would let two authenticated subjects that happen to collide on a
27
- # session id (client-supplied values, JWT jti reuse, etc.) upsert onto
28
- # each other's row.
29
- add_index :<%= table_name %>, [:<%= subject_column %>, :<%= session_column %>], unique: true
30
-
31
- # Session lookups (heartbeat, disconnect, session_status) always scope
32
- # by (subject, session); the composite unique index above serves those.
33
- # Query by subject alone
34
- add_index :<%= table_name %>, :<%= subject_column %>
35
-
36
- # Cleanup old records
37
- add_index :<%= table_name %>, :last_heartbeat
38
-
39
- <% if subject_table.present? -%>
40
- # Foreign key to subject table
41
- add_foreign_key :<%= table_name %>, :<%= subject_table %>, column: :<%= subject_column %>
42
- <% end -%>
43
- end
44
- end