path-reporting 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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