Module: CloseEncounters

Defined in:
lib/close_encounters.rb,
lib/close_encounters/engine.rb,
lib/close_encounters/version.rb,
lib/close_encounters/middleware.rb,
lib/close_encounters/adapters/net_http.rb,
app/models/close_encounters/participant_event.rb,
app/models/close_encounters/application_record.rb,
app/models/close_encounters/participant_service.rb

Defined Under Namespace

Modules: Adapters Classes: ApplicationRecord, Configuration, Engine, Middleware, ParticipantEvent, ParticipantService

Constant Summary collapse

VERIFIED_SIGNATURE =
"ok"
UNVERIFIED_SIGNATURE =
"unverified"
VERSION =
"0.3.0"

Class Method Summary collapse

Class Method Details

.auto_contact!Object

Enable automatic contact recording in the Rack Middleware



184
185
186
# File 'lib/close_encounters.rb', line 184

def auto_contact!
  configuration.auto_contact = true
end

.auto_contact?Boolean

Determine if contacts with third party services should be recorded automatically using the Rack Middleware

Set the CLOSE_ENCOUNTERS_AUTO_CONTACT environment variable to enable this feature or call CloseEncounters.auto_contact! in an initializer

Returns:

  • (Boolean)

    whether or not to automatically record contacts



176
177
178
179
180
181
# File 'lib/close_encounters.rb', line 176

def auto_contact?
  # If auto_contact is explicitly set, use that value
  return configuration.auto_contact unless configuration.auto_contact.nil?
  # Otherwise check the environment variable
  !!ENV["CLOSE_ENCOUNTERS_AUTO_CONTACT"]
end

.configurationObject



23
24
25
# File 'lib/close_encounters.rb', line 23

def self.configuration
  @configuration ||= Configuration.new
end

.configure {|configuration| ... } ⇒ Object

Yields:



33
34
35
# File 'lib/close_encounters.rb', line 33

def self.configure
  yield(configuration)
end

.contact(name, status:, response:) ⇒ Object

Record a contact with a third party service if the status has changed

Parameters:

  • name (String)

    the name of the service

  • status (Integer)

    the HTTP status of the contact

  • response (String)

    the response object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/close_encounters.rb', line 42

def contact(name, status:, response:)
  service = ParticipantService.find_by!(name:)
  status = status.to_i # Ensure status is always an integer

  created = nil
  # Use a transaction with a lock to prevent race conditions
  service.with_lock do
    unless service.events.newest.pick(:status) == status
      created = service.events.create!(status: status, response:)
    end
  end

  # Instrument after the transaction commits so subscribers see a persisted event.
  instrument(name, created) if created
  created
end

.deprecatorObject

Deprecator for CloseEncounters. Registered with the host app in the engine so warnings flow through the app's configured deprecation behavior.



29
30
31
# File 'lib/close_encounters.rb', line 29

def self.deprecator
  @deprecator ||= ActiveSupport::Deprecation.new("a future release", "CloseEncounters")
end

.ensure_service(name, connection_info: {}) ⇒ Object

Ensure that a participant service exists

Parameters:

  • name (String)

    the name of the service

  • connection_info (Hash) (defaults to: {})

    the connection information for the service



203
204
205
206
207
# File 'lib/close_encounters.rb', line 203

def ensure_service(name, connection_info: {})
  ParticipantService.find_or_create_by!(name: name) do |service|
    service.connection_info = connection_info unless service.connection_info.present?
  end
end

.record(name, response, adapter:, verifier: nil) ⇒ Object

Record the outcome of an HTTP request to a service straight from the client's response object, using an adapter to read the status and body.

An adapter is any object responding to #status(response) and #body(response); CloseEncounters::Adapters::NetHTTP ships for Net::HTTP. Delegates to #scan when a verifier is given, otherwise #contact.

CloseEncounters.record("SomeService", response, adapter: Adapters::NetHTTP)
CloseEncounters.record("SomeService", response, adapter: Adapters::NetHTTP, verifier: my_verifier)

Parameters:

  • name (String)

    the name of the service

  • response (Object)

    the HTTP client's response object

  • adapter (#status, #body)

    reads the status and body from the response

  • verifier (#call, #to_s, nil) (defaults to: nil)

    when given, records a verified scan



73
74
75
76
77
78
79
80
81
# File 'lib/close_encounters.rb', line 73

def record(name, response, adapter:, verifier: nil)
  status = adapter.status(response)
  body = adapter.body(response)
  if verifier
    scan(name, status:, response: body, verifier:)
  else
    contact(name, status:, response: body)
  end
end

.scan(name, status:, response:, verifier:) ⇒ Object Also known as: verify

Record a verification of a contact with a third party service where the verification is a callable which must also respond to to_s.

The verifier may return:

* true / :ok           — response is verified
* false / nil          — response failed verification (generic)
* any other value      — failed verification with a distinguishing
                       "signature" (e.g. "missing:user.email").
                       Stable signatures let scan suppress repeated
                       identical failures while still recording when
                       the failure mode changes meaningfully.

Creates a new event if:

  1. The status has changed from the last recorded status, OR
  2. The status is in the verify_scan list AND the verification signature differs from the last recorded signature.

Parameters:

  • name (String)

    the name of the service

  • status (Integer)

    the HTTP status of the contact

  • response (String)

    the response object

  • verifier (#call, #to_s)

    the verification callable



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/close_encounters.rb', line 123

def scan(name, status:, response:, verifier:)
  service = ParticipantService.find_by!(name:)
  status = status.to_i # Ensure status is always an integer

  created = nil
  service.with_lock do
    last_event = service.events.newest.first
    last_status = last_event&.status

    signature = signature_for(verifier.call(response))
    verified = (signature == VERIFIED_SIGNATURE)
    last_signature = signature_for_event(last_event)

     = {verified:, signature:, verification: verifier.to_s}

    if last_status != status
      created = service.events.create!(status:, response:, metadata:)
    elsif verify_scan_statuses.include?(status) && last_signature != signature
      created = service.events.create!(status:, response:, metadata:)
    end
  end

  instrument(name, created) if created
  created
end

.signature_for(result) ⇒ Object

Normalize a verifier return value into a stable signature string.



150
151
152
153
154
155
156
# File 'lib/close_encounters.rb', line 150

def signature_for(result)
  case result
  when true, :ok then VERIFIED_SIGNATURE
  when false, nil then UNVERIFIED_SIGNATURE
  else result.to_s
  end
end

.signature_for_event(event) ⇒ Object

Read the signature off a previously stored event, falling back to the legacy verified flag for events created before signatures existed.



160
161
162
163
164
165
# File 'lib/close_encounters.rb', line 160

def signature_for_event(event)
  return nil unless event
  stored = event..to_h["signature"]
  return stored if stored
  event.verified? ? VERIFIED_SIGNATURE : UNVERIFIED_SIGNATURE
end

.status(name) ⇒ Integer

Get the status of the most recent contact with a third party service

Parameters:

  • name (String)

    the name of the service

Returns:

  • (Integer)

    the HTTP status of the most recent contact



195
196
197
# File 'lib/close_encounters.rb', line 195

def status(name)
  ParticipantService.find_by!(name: name).events.newest.pick(:status)
end

.verify_scan_statusesObject

Get the statuses that should be verified



189
# File 'lib/close_encounters.rb', line 189

def verify_scan_statuses = configuration.verify_scan_statuses