path-reporting 0.1.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d921c4f5d8a011b077006a44cb6800d3f9d63329b46d750626aa9ff9fd17859
4
- data.tar.gz: 1828887be14dbb2b8cbbe5a382e36e14800fce6d5b306764234e0f1f974729ac
3
+ metadata.gz: d37d78cfbdf517535d51c48f43a69899a7d5c00b57dc76e6d450551ddc157ed3
4
+ data.tar.gz: 12f2ca0f0c8de09d5a0467793461ea2bd8a678c8086a63062f5e13b256958dca
5
5
  SHA512:
6
- metadata.gz: 213c4dffd2afaaf7589ce520aa6cbe78d9ecf998ef0528fe9f35e1539d0b547a292a5ec0c42cd9f6031b25ab31628baca91356abbcb0eda2ebbc6a4a7e702bd5
7
- data.tar.gz: 624e433365f0b3099c3c2d5e3eed73ca6aa2ba7648fd4df7590ebd578dc0752e1cea64a1b30073fe34b3d1386ca7dcde1683237db61e90d65981e3290c0504a0
6
+ metadata.gz: b4159af503e477b87299ac911e8e7d935cd2dff29b30af85a3a2d9e0492b52abae5ebf54adfb247a6b31a27225b1c6e095178f43785857e5b58a298cf4b21792
7
+ data.tar.gz: c8af462a40b0240a3b54da2793345ba25446a50a7699ef2ac39482992021618d570f1e7c73dcc677d1a796971382f1f66d6da8023c23d147f1948331492dae43
data/.yardopts CHANGED
@@ -1 +1 @@
1
- --no-private --plugin rspec
1
+ --no-private --plugin rspec -m markdown --title="Path Reporting"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- path-reporting (0.1.1)
4
+ path-reporting (0.1.2)
5
5
  amplitude-api
6
6
 
7
7
  GEM
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Path CCM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,8 +1,12 @@
1
- # Reporting
1
+ # Path Reporting
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/reporting`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pathccm/reporting/Ruby?style=flat-square) ![Gem](https://img.shields.io/gem/v/path-reporting?style=flat-square) ![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/rubygems/path-reporting?style=flat-square) ![GitHub](https://img.shields.io/github/license/pathccm/reporting?style=flat-square)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ The one stop shop for reporting at [Path](https://pathmentalhealth.com)
6
+
7
+ This gem contains (or will contain) all the various types of reporting we need
8
+ to do at Path. From metrics to analytics to performance and beyond, this gem
9
+ is meant to enable us to report anything we need as simply as possible.
6
10
 
7
11
  ## Installation
8
12
 
@@ -16,7 +20,35 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
20
 
17
21
  ## Usage
18
22
 
19
- TODO: Write usage instructions here
23
+ First you will need to initialize and configure the module
24
+
25
+ ```ruby
26
+ Path::Reporting.init do |config|
27
+ config.analytics.logger = Rails.logger
28
+ end
29
+ ```
30
+
31
+ See [our Configuration docs](https://www.rubydoc.info/gems/path-reporting/Path/Reporting/Configuration)
32
+ for more information on how to configure this module
33
+
34
+ ### Analytics
35
+
36
+ After initialing the module, record an analytics event with the following code.
37
+
38
+ ```ruby
39
+
40
+ PathReporting.analytics.record(
41
+ product_code: Constants::ANALYTICS_PRODUCT_CODE,
42
+ product_area: Constants::ANALYTICS_PRODUCT_AREA_MATCHING,
43
+ name: 'Preferred provider multiple valid matches',
44
+ user: @contact.analytics_friendly_hash,
45
+ user_type: PathReporting::UserType.PATIENT,
46
+ trigger: PathReporting::Trigger.PAGE_VIEW,
47
+ metadata: analytics_metadata,
48
+ )
49
+ ```
50
+
51
+ Be sure to read our [Analytics Guide (Internal Doc)](https://docs.google.com/document/d/1axnk1EkKCb__sxtvMomrPNup3wsviDOAefQWwXU3Z3U/edit#)
20
52
 
21
53
  ## Development
22
54
 
@@ -1,18 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "channels/amplitude"
4
- require_relative "channels/console"
3
+ require_relative "reporters/amplitude"
4
+ require_relative "reporters/console"
5
5
  require_relative "../types/trigger"
6
6
  require_relative "../types/user_type"
7
7
 
8
8
  module Path
9
9
  module Reporting
10
+ # Our primary class for reporting analytics data. Once configured, this
11
+ # class can report analytics to any and all enabled and configured
12
+ # reporters.
13
+ #
14
+ # This class is not a singleton, but is exposed via the {Reporting.analytics}
15
+ # property once the Reporting module is initialized
10
16
  class Analytics
17
+ # Create a new analytics reporter with the given configuration
18
+ # @param config [Analytics::Configuration] configuration to use
11
19
  def initialize(config)
12
20
  @config = config
13
21
  end
14
22
 
15
23
  # Get clients for reporting events based on environment
24
+ # @private
16
25
  def clients
17
26
  if @clients.nil?
18
27
  @clients = {
@@ -24,19 +33,28 @@ module Path
24
33
  @clients
25
34
  end
26
35
 
36
+ # @private
27
37
  def setup_amplitude
28
38
  # Amplitude reporting for metrics
29
- return Path::Reporting::Analytics::Channels::Amplitude.new @config if @config.amplitude_enabled?
39
+ return Path::Reporting::Analytics::Amplitude.new @config if @config.amplitude_enabled?
30
40
 
31
41
  nil
32
42
  end
33
43
 
44
+ # @private
34
45
  def setup_console
35
- return Path::Reporting::Analytics::Channels::Console.new @config if @config.console_enabled?
46
+ return Path::Reporting::Analytics::Console.new @config if @config.console_enabled?
36
47
 
37
48
  nil
38
49
  end
39
50
 
51
+ # Normalize the event name for easier searching in our analytics tools
52
+ #
53
+ # Generally the format is:
54
+ # - Product code: All uppercase, _ separator between words
55
+ # - Product area: Upper camel case, no spaces or separators
56
+ # - (Event) Name: Sentence case, _ separator between words
57
+ # @private
40
58
  def format_event_name(product_code:, product_area:, name:)
41
59
  formatted_code = product_code.gsub(" ", "_").upcase
42
60
  formatted_area = product_area.split(" ").each(&:capitalize!).join("")
@@ -44,6 +62,97 @@ module Path
44
62
  "#{formatted_code}_#{formatted_area}_#{formatted_desc}"
45
63
  end
46
64
 
65
+ # Record analytics data to our enabled analytics channel
66
+ #
67
+ # This is the primary (and at the moment only) way to report analytics
68
+ # data in our systems. Every configured analytics reporter will record
69
+ # the event with the data given here.
70
+ #
71
+ # @note ***No patient PII or PHI is allowed in our analytics data.***
72
+ # By default we will attempt to strip this out of user data as well
73
+ # as metadata, but this is imperfect and should not be relied on.
74
+ # Instead, proactively exclude that data before it gets here.
75
+ # @note The `product_code`, `product_area`, and `name` parameters will
76
+ # be formatted for easier searching automatically. Feel free to use
77
+ # regular text formatting here. E.g. "Self Scheduling" or "Hold booked"
78
+ #
79
+ # @param product_code [String] Code denoting which product this event
80
+ # occurred within. For example, "Self Scheduling". Can be used to view
81
+ # all the events that happen in a specific product
82
+ #
83
+ # Bias names toward whatever the major user-facing component is. For
84
+ # example, Therapy or Operations.
85
+ # @param product_area [String] Area of the product that event relates to.
86
+ # For example, “Appointment Booking” or “User Settings”. Can be used to
87
+ # see all events in a particular product area, as well as in rare cases
88
+ # be used across product codes to show events across the system.
89
+ #
90
+ # Bias this name toward the particular flow or feature being used,
91
+ # e.g. Scheduling
92
+ # @param name [String] Short plain text description of the event that is
93
+ # being recorded. For example, “Hold booked”
94
+ #
95
+ # Follow the format: “Object action [descriptor]”. For example,
96
+ # “Hold converted to appointment” or “Appointment deleted”
97
+ # @param user [Hash] A simple hash containing relevant user information.
98
+ # **This information should never contain any PII or PHI**. There does,
99
+ # however, need to be an `id` property to uniquely identify the user.
100
+ #
101
+ # One of the following (in order):
102
+ #
103
+ # 1. Patient/User
104
+ # 2. If it does not relate to a patient, Provider
105
+ # 3. If it does not relate to a patient or provider, Insurer
106
+ # 4. If it does not relate to a patient, provider, or insurer, use external system ID information
107
+ # - E.g. for Zocdoc, this maybe the primary key for their API key
108
+ # - Do not use an API key as an identifier, instead use another key like the id in our database for that key
109
+ #
110
+ # If a different user (like an ops person) took an action, put an ID
111
+ # for them under `agent_id` in the metadata
112
+ # @param user_type [Reporting::UserType] The type of user we are reporting
113
+ # this event for
114
+ # @param trigger [Reporting::Trigger] What triggered this event to be
115
+ # recoreded (e.g. a page view, or an interaction).
116
+ # @param metadata [Hash] Metadata to report alongside the analytics event.
117
+ # **This should not contain any PII or PHI*
118
+ #
119
+ # @example
120
+ # PathReporting.analytics.record(
121
+ # product_code: Constants::ANALYTICS_PRODUCT_CODE,
122
+ # product_area: Constants::ANALYTICS_PRODUCT_AREA_MATCHING,
123
+ # name: 'Preferred provider multiple valid matches',
124
+ # user: @contact.analytics_friendly_hash,
125
+ # user_type: PathReporting::UserType.PATIENT,
126
+ # trigger: PathReporting::Trigger.PAGE_VIEW,
127
+ # metadata: analytics_metadata,
128
+ # )
129
+ # @example Validating Successful Reporting
130
+ # analytics_reported = PathReporting.analytics.record(
131
+ # product_code: Constants::ANALYTICS_PRODUCT_CODE,
132
+ # product_area: Constants::ANALYTICS_PRODUCT_AREA_MATCHING,
133
+ # name: 'No preferred provider',
134
+ # user: @contact.analytics_friendly_hash,
135
+ # user_type: PathReporting::UserType.PATIENT,
136
+ # trigger: PathReporting::Trigger.PAGE_VIEW,
137
+ # )
138
+ #
139
+ # analytics_reported.each do |status|
140
+ # Rails.logger.warn("#{status.reporter} failed") unless status.result.nil?
141
+ # end
142
+ #
143
+ # @raise [StandardError] if no user is provided or user does not have id
144
+ # @raise [StandardError] if user_type is not a Reporting::UserType
145
+ # @raise [StandardError] if trigger is not a Reporting::Trigger
146
+ #
147
+ # @return [Array] An array of result hashes with two keys:
148
+ #
149
+ # - `reporter` [String] the analytics reporter the result is for
150
+ # - `result` [nil | StandardError] what the result of running this
151
+ # reporter was. If it did not run, it will always be `nil`
152
+ #
153
+ # @see https://docs.google.com/document/d/1axnk1EkKCb__sxtvMomrPNup3wsviDOAefQWwXU3Z3U/edit# Analytics Guide
154
+ # @see Reporting::UserType
155
+ # @see Reporting::Trigger
47
156
  def record(
48
157
  product_code:,
49
158
  product_area:,
@@ -53,37 +162,36 @@ module Path
53
162
  trigger: Trigger.INTERACTION,
54
163
  metadata: {}
55
164
  )
56
- throw Error("No user provided when reporting analytics") unless user
165
+ throw Error("No user provided when reporting analytics") if !user || !user[:id]
57
166
  throw Error("Invalid UserType #{user_type}") unless UserType.valid?(user_type)
58
167
  throw Error("Invalid Trigger #{trigger}") unless Trigger.valid?(trigger)
59
168
 
60
- all_succeeded = true
61
- exceptions = {}
62
- clients.each do |_reporter, client|
63
- next if client.nil?
64
-
65
- begin
66
- event_name = format_event_name(product_code: product_code, product_area: product_area, name: name)
67
- client.record(
68
- name: event_name,
69
- user: user,
70
- user_type: user_type,
71
- metadata: metadata,
72
- trigger: trigger
73
- )
74
- exceptions[client.channel_name] = nil
75
- rescue StandardError => e
76
- all_succeeded = false
77
- exceptions[client.channel_name] = e
78
- end
169
+ clients.map do |reporter, client|
170
+ {
171
+ reporter: reporter.to_s,
172
+ result: send_event_to_client(client, {
173
+ name: format_event_name(product_code: product_code, product_area: product_area, name: name),
174
+ user: user,
175
+ user_type: user_type,
176
+ metadata: metadata,
177
+ trigger: trigger
178
+ })
179
+ }
79
180
  end
181
+ end
80
182
 
81
- {
82
- all_succeeded: all_succeeded,
83
- exceptions: exceptions
84
- }
183
+ # Wraps sending to the client in a rescue so we can report results
184
+ # without causing other reporters to not run
185
+ # @private
186
+ def send_event_to_client(client, event)
187
+ client&.record(**event)
188
+ nil
189
+ rescue StandardError => e
190
+ e
85
191
  end
86
192
 
193
+ # Primarily used for testing
194
+ # @private
87
195
  def reset!
88
196
  @clients = nil
89
197
  end
@@ -3,28 +3,46 @@
3
3
  module Path
4
4
  module Reporting
5
5
  class Analytics
6
+ # Configuration for analytics reporting. Generally this is for
7
+ # configuring amplitude and/or a logger that analytics should
8
+ # be reported to
9
+ # @!attribute amplitude_config
10
+ # Set the configuration for the AmplitudeAPI gem
11
+ # @return [Hash] amplitude configuration options as passed along to the amplitude-api gem
12
+ # @see https://www.rubydoc.info/gems/amplitude-api/AmplitudeAPI/Config AmplitudeAPI::Config
13
+ # @!attribute logger
14
+ # The logger for the console logging analytics reporter
15
+ # @return [#info] a logging interface with a .info method we can log analytics to
16
+ # @example
17
+ # conf.logger = Rails.logger
6
18
  class Configuration
7
- attr_accessor :console_enabled
8
- attr_reader :amplitude_config, :console_logger
19
+ attr_reader :amplitude_config, :logger
9
20
 
21
+ # New Configuration with all reporting off by default
10
22
  def initialize
11
23
  @console_enabled = false
12
- @console_logger = nil
24
+ @logger = nil
13
25
  @amplitude_enabled = false
14
26
  @amplitude_config = nil
15
27
  end
16
28
 
17
29
  def logger=(logger)
18
30
  @console_enabled = !logger.nil?
19
- @console_logger = logger
31
+ @logger = logger
20
32
  end
21
33
 
22
34
  alias console= logger=
23
35
 
36
+ # Check if the logger has been configured and is available for use
37
+ # @return [Boolean] if logger is available for use
24
38
  def console_enabled?
25
- @console_enabled && @console_logger
39
+ @console_enabled && @logger
26
40
  end
27
41
 
42
+ # Set the configuration for the AmplitudeAPI gem
43
+ # @param conf [Hash] configuration options for amplitude
44
+ # @return [Hash] amplitude configuration options as passed along to the amplitude-api gem
45
+ # @see https://www.rubydoc.info/gems/amplitude-api/AmplitudeAPI/Config AmplitudeAPI::Config
28
46
  def amplitude_config=(conf)
29
47
  @amplitude_enabled = !conf.nil?
30
48
  @amplitude_config = conf
@@ -32,6 +50,8 @@ module Path
32
50
 
33
51
  alias amplitude= amplitude_config=
34
52
 
53
+ # Check if Amplitude has been configured and is available for use
54
+ # @return [Boolean] if Amplitude is available for use
35
55
  def amplitude_enabled?
36
56
  @amplitude_enabled && @amplitude_config
37
57
  end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "amplitude-api"
4
+
5
+ # See https://developers.amplitude.com/docs/http-api-v2#schemauploadrequestbody
6
+ API_METADATA_TO_ELEVATE = [
7
+ "device_id",
8
+ "app_version",
9
+ "platform",
10
+ "os_name",
11
+ "os_version",
12
+ "device_brand",
13
+ "device_manufacturer",
14
+ "device_model",
15
+ "carrier",
16
+ "country",
17
+ "region",
18
+ # We do not elevate city because at that level it is possibly PII
19
+ "dma",
20
+ "language",
21
+ "price",
22
+ "quantity",
23
+ "revenue",
24
+ "productId",
25
+ "revenueType",
26
+ # We do not elevate lat/long/IP because it is PII (IP at least for analytics)
27
+ "event_id",
28
+ "session_id",
29
+ "insert_id",
30
+ "plan"
31
+ ].freeze
32
+
33
+ # Amplitue is not HIPAA compliant, so there are a number of PII things we want
34
+ # to make sure to filter out. These are keys (all lowercase) that are things
35
+ # we want to filter. Lowercased matching keys in data are obfuscated.
36
+ DISALLOWED_METADATA_PII_KEYS = %w[
37
+ email
38
+ name
39
+ first_name
40
+ firstname
41
+ last_name
42
+ lastname
43
+ zip
44
+ ssn
45
+ dob
46
+ address
47
+ phone
48
+ contactinfo
49
+ patient_chart_id
50
+ ].freeze
51
+
52
+ # Don't clean to infinity (and beyond)
53
+ MAX_METADATA_DEPTH = 4
54
+
55
+ module Path
56
+ module Reporting
57
+ class Analytics
58
+ # Amplitude analytics is our primary analytics channel for production
59
+ class Amplitude
60
+ # Setup and configure AmplitudeAPI with the given configuration
61
+ # @param config [AmplitudeAPI::Config] the configuration for AmplitudeAPI
62
+ # @see https://www.rubydoc.info/gems/amplitude-api/AmplitudeAPI/Config AmplitudeAPI::Config Documentation
63
+ # @see https://github.com/toothrot/amplitude-api AmplitudeAPI repository
64
+ def initialize(config)
65
+ @config = config
66
+
67
+ config.amplitude_config.each do |key, value|
68
+ AmplitudeAPI.config.instance_variable_set("@#{key}", value)
69
+ end
70
+ end
71
+
72
+ # Record the metadata to Amplitude
73
+ # @param name [String] Formatted name to send to Amplitude
74
+ # @param user [Hash] User object. Must contain `:id` and no PII
75
+ # @param user_type [UserType] Type of `user`
76
+ # @param trigger [Trigger] Trigger for this event
77
+ # @param metadata [Hash] Metadata to send with the event
78
+ # @see Analytics
79
+ def record(name:, user:, user_type:, trigger:, metadata: {})
80
+ user = user.dup
81
+ metadata = metadata.dup
82
+ user[:user_type] = user_type
83
+ metadata[:trigger] = trigger
84
+
85
+ event_props = {
86
+ user_id: user[:id].to_s,
87
+ user_properties: scrub_pii(user),
88
+ event_type: name,
89
+ event_properties: scrub_pii(metadata)
90
+ }
91
+ API_METADATA_TO_ELEVATE.each do |key|
92
+ event_props[key] = metadata[key] if metadata.key? key
93
+ end
94
+
95
+ AmplitudeAPI.track AmplitudeAPI::Event.new event_props
96
+ end
97
+
98
+ # Scrub known PII keys from a hash
99
+ # @private
100
+ def scrub_pii(data, depth: 0)
101
+ return "[DATA]" if depth > MAX_METADATA_DEPTH
102
+
103
+ case data
104
+ when Hash
105
+ data = data.dup
106
+ # Replace any disallowed keys, and recurse for allowed values
107
+ data.each do |key, val|
108
+ data[key] = if DISALLOWED_METADATA_PII_KEYS.include? key.to_s
109
+ "XXXXXXXX"
110
+ else
111
+ scrub_pii(val, depth: depth + 1)
112
+ end
113
+ end
114
+ when Array
115
+ return data.map { |item| scrub_pii(item, depth: depth + 1) }
116
+ end
117
+
118
+ data
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Path
4
+ module Reporting
5
+ class Analytics
6
+ # Simple non-structure console logging for analytics data. Most helpful
7
+ # for development or backup rather than analysis.
8
+ class Console
9
+ # Create new console logging analytics reporter
10
+ # @param config [Analytics::Configuration] the configuration for the reporter
11
+ # @see Analytics::Configuration
12
+ def initialize(config)
13
+ @config = config
14
+ end
15
+
16
+ # Log the analytics event to the configured logger
17
+ # @param name [String] Formatted name to send to Amplitude
18
+ # @param user [Hash] User object. Must contain `:id` and no PII
19
+ # @param user_type [UserType] Type of `user`
20
+ # @param trigger [Trigger] Trigger for this event
21
+ # @param metadata [Hash] Metadata to send with the event
22
+ # @see Analytics
23
+ def record(name:, user:, user_type:, trigger:, metadata: {})
24
+ @config.logger.info("[#{trigger}]:#{name} - #{user.inspect} (#{user_type}) #{metadata.nil? ? "" : metadata.inspect}")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/configuration.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "analytics/configuration"
4
4
 
5
5
  module Path
6
6
  module Reporting
7
+ # Global configuration for all reporting sub-modules
7
8
  # @!attribute [r] analytics
8
9
  # @return [Analytics::Configuration] the configuration for analytics reporting
9
10
  class Configuration
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Path
4
4
  module Reporting
5
- VERSION = "0.1.1"
5
+ # Current version of the module
6
+ VERSION = "0.1.2"
6
7
  end
7
8
  end
data/lib/reporting.rb CHANGED
@@ -12,6 +12,7 @@ module Path
12
12
  # meant to be a one-stop shop for all of our ruby code to import and have
13
13
  # everything they need to track all the things
14
14
  module Reporting
15
+ # @private
15
16
  class Error < StandardError; end
16
17
 
17
18
  class << self
data/lib/types/trigger.rb CHANGED
@@ -2,12 +2,20 @@
2
2
 
3
3
  module Path
4
4
  module Reporting
5
+ # The trigger or cause of reporting events
5
6
  class Trigger
6
7
  class << self
8
+ # Interaction: When a direct intentional user action is the cause of
9
+ # this event
7
10
  INTERACTION = "Interaction"
11
+ # Page view: When the event was an indirect result of viewing something.
12
+ # @note Because of usage limits, we do not want to record page views
13
+ # as a separate action, this is only for indirect consequences
8
14
  PAGE_VIEW = "Page view"
15
+ # Automation: Some automation or tool was the cause of this event
9
16
  AUTOMATION = "Automation"
10
17
 
18
+ # @private
11
19
  def triggers
12
20
  [
13
21
  INTERACTION,
@@ -16,6 +24,8 @@ module Path
16
24
  ]
17
25
  end
18
26
 
27
+ # Check if a given item is a valid Trigger
28
+ # @param maybe_trigger [Any] item to check
19
29
  def valid?(maybe_trigger)
20
30
  triggers.includes? maybe_trigger
21
31
  end
@@ -2,15 +2,23 @@
2
2
 
3
3
  module Path
4
4
  module Reporting
5
+ # User types that data may be recorded for or on
5
6
  class UserType
6
7
  class << self
8
+ # Patient or potential patient
7
9
  PATIENT = "Patient"
10
+ # Provider, which can be any sub-type (e.g. therapist)
8
11
  PROVIDER = "Provider"
12
+ # Insurer, not currently in-use
9
13
  INSURER = "Insurer"
14
+ # Operator or any internal non-developer
10
15
  OPERATOR = "Operator"
16
+ # Developer; mostly relevant for backfills or manual intervention
11
17
  DEVELOPER = "Developer"
18
+ # System, either first-party or third-party
12
19
  SYSTEM = "System"
13
20
 
21
+ # @private
14
22
  def types
15
23
  [
16
24
  PATIENT,
@@ -22,6 +30,8 @@ module Path
22
30
  ]
23
31
  end
24
32
 
33
+ # Check if a given item is a valid UserType
34
+ # @param maybe_type [Any] item to check
25
35
  def valid?(maybe_type)
26
36
  types.includes? maybe_type
27
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: path-reporting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Hushbeck
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-28 00:00:00.000000000 Z
11
+ date: 2022-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: amplitude-api
@@ -50,12 +50,13 @@ files:
50
50
  - ".yardopts"
51
51
  - Gemfile
52
52
  - Gemfile.lock
53
+ - LICENSE.md
53
54
  - README.md
54
55
  - Rakefile
55
56
  - lib/analytics/analytics.rb
56
- - lib/analytics/channels/amplitude.rb
57
- - lib/analytics/channels/console.rb
58
57
  - lib/analytics/configuration.rb
58
+ - lib/analytics/reporters/amplitude.rb
59
+ - lib/analytics/reporters/console.rb
59
60
  - lib/configuration.rb
60
61
  - lib/reporting.rb
61
62
  - lib/reporting/version.rb
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "amplitude-api"
4
-
5
- # See https://developers.amplitude.com/docs/http-api-v2#schemauploadrequestbody
6
- API_METADATA_TO_ELEVATE = [
7
- "device_id",
8
- "app_version",
9
- "platform",
10
- "os_name",
11
- "os_version",
12
- "device_brand",
13
- "device_manufacturer",
14
- "device_model",
15
- "carrier",
16
- "country",
17
- "region",
18
- # We do not elevate city because at that level it is possibly PII
19
- "dma",
20
- "language",
21
- "price",
22
- "quantity",
23
- "revenue",
24
- "productId",
25
- "revenueType",
26
- # We do not elevate lat/long/IP because it is PII (IP at least for analytics)
27
- "event_id",
28
- "session_id",
29
- "insert_id",
30
- "plan"
31
- ].freeze
32
-
33
- # Amplitue is not HIPAA compliant, so there are a number of PII things we want
34
- # to make sure to filter out. These are keys (all lowercase) that are things
35
- # we want to filter. Lowercased matching keys in data are obfuscated.
36
- DISALLOWED_METADATA_PII_KEYS = %w[
37
- email
38
- name
39
- first_name
40
- firstname
41
- last_name
42
- lastname
43
- zip
44
- ssn
45
- dob
46
- address
47
- phone
48
- contactinfo
49
- patient_chart_id
50
- ].freeze
51
-
52
- # Don't clean to infinity (and beyond)
53
- MAX_METADATA_DEPTH = 4
54
-
55
- module Path
56
- module Reporting
57
- class Analytics
58
- module Channels
59
- # Amplitude analytics is our primary analytics channel for production
60
- class Amplitude
61
- attr_reader :channel_name
62
-
63
- def initialize(config)
64
- @channel_name = "Amplitude"
65
- @config = config
66
-
67
- config.amplitude_config.each do |key, value|
68
- AmplitudeAPI.config.instance_variable_set("@#{key}", value)
69
- end
70
- end
71
-
72
- def record(name:, user:, user_type:, trigger:, metadata: {})
73
- user = user.dup
74
- metadata = metadata.dup
75
- user[:user_type] = user_type
76
- metadata[:trigger] = trigger
77
-
78
- event_props = {
79
- user_id: user[:id].to_s,
80
- user_properties: scrub_pii(user),
81
- event_type: name,
82
- event_properties: scrub_pii(metadata)
83
- }
84
- API_METADATA_TO_ELEVATE.each do |key|
85
- event_props[key] = metadata[key] if metadata.key? key
86
- end
87
-
88
- AmplitudeAPI.track AmplitudeAPI::Event.new event_props
89
- end
90
-
91
- def scrub_pii(data, depth: 0)
92
- return "[DATA]" if depth > MAX_METADATA_DEPTH
93
-
94
- case data
95
- when Hash
96
- data = data.dup
97
- # Replace any disallowed keys, and recurse for allowed values
98
- data.each do |key, val|
99
- data[key] = if DISALLOWED_METADATA_PII_KEYS.include? key.to_s
100
- "XXXXXXXX"
101
- else
102
- scrub_pii(val, depth: depth + 1)
103
- end
104
- end
105
- when Array
106
- return data.map { |item| scrub_pii(item, depth: depth + 1) }
107
- end
108
-
109
- data
110
- end
111
- end
112
- end
113
- end
114
- end
115
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Path
4
- module Reporting
5
- class Analytics
6
- module Channels
7
- class Console
8
- attr_reader :channel_name
9
-
10
- def initialize(config)
11
- @channel_name = "Console"
12
- @config = config
13
- end
14
-
15
- def record(name:, user:, user_type:, trigger:, metadata: {})
16
- @config.console_logger.info("[#{trigger}]:#{name} - #{user.inspect} (#{user_type}) #{metadata.nil? ? "" : metadata.inspect}")
17
- end
18
- end
19
- end
20
- end
21
- end
22
- end