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 +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
|
+
![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
|
-
|
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
|