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 +4 -4
- data/.yardopts +1 -1
- data/Gemfile.lock +1 -1
- data/LICENSE.md +21 -0
- data/README.md +36 -4
- data/lib/analytics/analytics.rb +136 -28
- data/lib/analytics/configuration.rb +25 -5
- data/lib/analytics/reporters/amplitude.rb +123 -0
- data/lib/analytics/reporters/console.rb +29 -0
- data/lib/configuration.rb +1 -0
- data/lib/reporting/version.rb +2 -1
- data/lib/reporting.rb +1 -0
- data/lib/types/trigger.rb +10 -0
- data/lib/types/user_type.rb +10 -0
- metadata +5 -4
- data/lib/analytics/channels/amplitude.rb +0 -115
- data/lib/analytics/channels/console.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d37d78cfbdf517535d51c48f43a69899a7d5c00b57dc76e6d450551ddc157ed3
|
4
|
+
data.tar.gz: 12f2ca0f0c8de09d5a0467793461ea2bd8a678c8086a63062f5e13b256958dca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
-
|
3
|
+
   
|
4
4
|
|
5
|
-
|
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
|
-
|
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
|
|
data/lib/analytics/analytics.rb
CHANGED
@@ -1,18 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
4
|
-
require_relative "
|
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::
|
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::
|
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")
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
@
|
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 && @
|
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
data/lib/reporting/version.rb
CHANGED
data/lib/reporting.rb
CHANGED
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
|
data/lib/types/user_type.rb
CHANGED
@@ -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.
|
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-
|
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
|