snowly 0.1.0 → 0.1.1

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
  !binary "U0hBMQ==":
3
- metadata.gz: 6ae21b19ffd1c668278584436ee8ebbb0fa24edc
4
- data.tar.gz: 14db8191b04c4dfadd468a55aa93bebf42c8c0f8
3
+ metadata.gz: d41577c932e8b4ba8f150d57bfe887f4be951c6c
4
+ data.tar.gz: 84a1422730f60bb6134ff28a6e6310a699fcd9e2
5
5
  SHA512:
6
- metadata.gz: a175ef4ab3507f5461de4baaa5aed6c1555a9bbe406d686f09fc2ab3eefa55a496a9f66713d1aa17a897bcf7ab227131c0cb2fa883975b2b872977ed042f6441
7
- data.tar.gz: d524621891fc5b22940cec02655decd2d627906283307c22e2ed9b351ff907a2458953628078ad04673f727629ad262c6f7946fb3367a02290e3aae5561d6e1e
6
+ metadata.gz: 749b0bb7137ff4782bece6bde1dfdc8fd5ed18db9002b0f9f28e9c4e5f186da137b91c448b8d3b289e638108da3e7d8117eea96487231abb7583ed940a670bbd
7
+ data.tar.gz: 151aff343e3031e30158aebe463fb079b94d2fa05eba04a9f8495659e5d30462417105197a4b17af14d57c9ad9314bf1a9260b395f954b36c66566fde78ab43a
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  /personal
11
+ *.gem
data/bin/snowly ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # /snowly
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ Bundler.setup
6
+ require 'snowly'
7
+ require 'snowly/app/collector'
8
+ require 'vegas'
9
+
10
+ Vegas::Runner.new(Snowly::App::Collector, 'collector')
data/config.ru ADDED
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+ require 'snowly'
5
+ require 'snowly/app/collector'
6
+ run Snowly::App::Collector
@@ -0,0 +1,265 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "id": "snowplow_protocol.json",
4
+ "description": "Representation of Snowplow Protocol in JSON Schema format for validation",
5
+ "type": "object",
6
+ "properties": {
7
+ "name_tracker": {
8
+ "type": "string",
9
+ "maxLength": 128
10
+ },
11
+ "event_vendor": {
12
+ "type": "string",
13
+ "maxLength": 1000
14
+ },
15
+ "app_id": {
16
+ "type": "string",
17
+ "maxLength": 255
18
+ },
19
+ "platform": {
20
+ "type": "string",
21
+ "enum": ["web", "mob", "pc", "srv", "tv", "cnsl", "iot"],
22
+ "maxLength": 255
23
+ },
24
+ "dvce_created_tstamp": {
25
+ "type": "integer"
26
+ },
27
+ "dvce_sent_tstamp": {
28
+ "type": "integer"
29
+ },
30
+ "true_tstamp": {
31
+ "type": "integer"
32
+ },
33
+ "os_timezone": {
34
+ "type": "string",
35
+ "maxLength": 255
36
+ },
37
+ "event": {
38
+ "type": "string",
39
+ "enum": ["se", "ev", "ue", "ad", "tr", "ti", "pv", "pp"]
40
+ },
41
+ "txn_id": {
42
+ "type": "integer"
43
+ },
44
+ "event_id": {
45
+ "type": "string",
46
+ "maxLength": 36
47
+ },
48
+ "v_tracker": {
49
+ "type": "string",
50
+ "maxLength": 100
51
+ },
52
+ "domain_userid": {
53
+ "type": "string",
54
+ "maxLength": 36
55
+ },
56
+ "network_userid": {
57
+ "type": "string",
58
+ "maxLength": 38
59
+ },
60
+ "user_id": {
61
+ "type": "string",
62
+ "maxLength": 255
63
+ },
64
+ "domain_sessionidx": {
65
+ "type": "integer"
66
+ },
67
+ "domain_sessionid": {
68
+ "type": "string",
69
+ "maxLength": 36
70
+ },
71
+ "user_ipaddress": {
72
+ "type": "string",
73
+ "maxLength": 45
74
+ },
75
+ "screen_res_width_x_height": {
76
+ "type": "string",
77
+ "pattern": "^[0-9]+x[0-9]+$"
78
+ },
79
+ "page_url": {
80
+ "type": "string",
81
+ "maxLength": 4096
82
+ },
83
+ "useragent": {
84
+ "type": "string",
85
+ "maxLength": 1000
86
+ },
87
+ "page_title": {
88
+ "type": "string",
89
+ "maxLength": 2000
90
+ },
91
+ "page_referer": {
92
+ "type": "string",
93
+ "maxLength": 4096
94
+ },
95
+ "user_fingerprint": {
96
+ "type": "integer"
97
+ },
98
+ "br_cookies": {
99
+ "type": "string",
100
+ "enum": ["1", "0"]
101
+ },
102
+ "br_lang": {
103
+ "type": "string",
104
+ "maxLength": 255
105
+ },
106
+ "br_features_pdf": {
107
+ "type": "string",
108
+ "enum": ["1", "0"]
109
+ },
110
+ "br_features_quicktime": {
111
+ "type": "string",
112
+ "enum": ["1", "0"]
113
+ },
114
+ "br_features_realplayer": {
115
+ "type": "string",
116
+ "enum": ["1", "0"]
117
+ },
118
+ "br_features_windowsmedia": {
119
+ "type": "string",
120
+ "enum": ["1", "0"]
121
+ },
122
+ "br_features_director": {
123
+ "type": "string",
124
+ "enum": ["1", "0"]
125
+ },
126
+ "br_features_flash": {
127
+ "type": "string",
128
+ "enum": ["1", "0"]
129
+ },
130
+ "br_features_java": {
131
+ "type": "string",
132
+ "enum": ["1", "0"]
133
+ },
134
+ "br_features_gears": {
135
+ "type": "string",
136
+ "enum": ["1", "0"]
137
+ },
138
+ "br_features_silverlight": {
139
+ "type": "string",
140
+ "enum": ["1", "0"]
141
+ },
142
+ "br_colordepth": {
143
+ "type": "integer"
144
+ },
145
+ "doc_width_x_height": {
146
+ "type": "string",
147
+ "pattern": "^[0-9]+x[0-9]+$"
148
+ },
149
+ "doc_charset": {
150
+ "type": "string",
151
+ "maxLength": 128
152
+ },
153
+ "browser_viewport_width_x_height": {
154
+ "type": "string",
155
+ "pattern": "^[0-9]+x[0-9]+$"
156
+ },
157
+ "mac_address": {
158
+ "type": "string",
159
+ "maxLength": 36
160
+ },
161
+ "pp_xoffset_min": {
162
+ "type": "integer"
163
+ },
164
+ "pp_xoffset_max": {
165
+ "type": "integer"
166
+ },
167
+ "pp_yoffset_min": {
168
+ "type": "integer"
169
+ },
170
+ "pp_yoffset_max": {
171
+ "type": "integer"
172
+ },
173
+ "tr_orderid": {
174
+ "type": "string",
175
+ "maxLength": 255
176
+ },
177
+ "tr_affiliation": {
178
+ "type": "string",
179
+ "maxLength": 255
180
+ },
181
+ "tr_total": {
182
+ "type": "number"
183
+ },
184
+ "tr_tax": {
185
+ "type": "number"
186
+ },
187
+ "tr_shipping": {
188
+ "type": "number"
189
+ },
190
+ "tr_city": {
191
+ "type": "string",
192
+ "maxLength": 255
193
+ },
194
+ "tr_state": {
195
+ "type": "string",
196
+ "maxLength": 255
197
+ },
198
+ "tr_country": {
199
+ "type": "string",
200
+ "maxLength": 255
201
+ },
202
+ "tr_currency": {
203
+ "type": "string",
204
+ "maxLength": 255
205
+ },
206
+ "ti_orderid": {
207
+ "type": "string",
208
+ "maxLength": 255
209
+ },
210
+ "ti_sku": {
211
+ "type": "string",
212
+ "maxLength": 255
213
+ },
214
+ "ti_name": {
215
+ "type": "string",
216
+ "maxLength": 255
217
+ },
218
+ "ti_category": {
219
+ "type": "string",
220
+ "maxLength": 255
221
+ },
222
+ "ti_price": {
223
+ "type": "number"
224
+ },
225
+ "ti_quantity": {
226
+ "type": "integer"
227
+ },
228
+ "ti_currency": {
229
+ "type": "string",
230
+ "maxLength": 255
231
+ },
232
+ "se_category": {
233
+ "type": "string",
234
+ "maxLength": 255
235
+ },
236
+ "se_action": {
237
+ "type": "string",
238
+ "maxLength": 255
239
+ },
240
+ "se_label": {
241
+ "type": "string",
242
+ "maxLength": 255
243
+ },
244
+ "se_property": {
245
+ "type": "string",
246
+ "maxLength": 255
247
+ },
248
+ "se_value": {
249
+ "type": "number"
250
+ }
251
+ },
252
+ "required": ["app_id", "platform", "event", "event_id", "v_tracker", "useragent"],
253
+ "custom_dependencies": {
254
+ "se_category": { "event": "se" },
255
+ "se_action": {"event": "se" },
256
+ "tr_orderid": { "event": "tr" },
257
+ "tr_total": { "event": "tr" },
258
+ "ti_orderid": { "event": "ti" },
259
+ "ti_sku": { "event": "ti" },
260
+ "ti_quantity": { "event": "ti" },
261
+ "ti_price": { "event": "ti" },
262
+ "unstruct_event": { "event": "ue"},
263
+ "page_url": { "platform": "web" }
264
+ }
265
+ }
@@ -0,0 +1,42 @@
1
+ require 'erb'
2
+ require 'base64'
3
+ require 'sinatra'
4
+ require "sinatra/reloader" if development?
5
+
6
+ module Snowly
7
+ module App
8
+ class Collector < Sinatra::Base
9
+ GIF = Base64.decode64("R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==")
10
+ configure :development do
11
+ register Sinatra::Reloader
12
+ end
13
+
14
+ get '/' do
15
+ @url = request.url.gsub(/(http|https)\:\/\//,'')[0..-2]
16
+ @resolved_schemas = if resolver = Snowly.local_iglu_resolver_path
17
+ Dir[File.join(resolver,"/**/*")].select{ |e| File.file? e }
18
+ else
19
+ nil
20
+ end
21
+ erb :index
22
+ end
23
+
24
+ get '/i' do
25
+ content_type :json
26
+ validator = Snowly::Validator.new request.query_string
27
+ if validator.validate
28
+ status 200
29
+ if params[:debug] || Snowly.debug_mode
30
+ body({ content: validator.request.as_hash }.to_json)
31
+ else
32
+ content_type 'image/gif'
33
+ Snowly::App::Collector::GIF
34
+ end
35
+ else
36
+ status 500
37
+ body ({ errors: validator.errors, content: validator.request.as_hash }.to_json)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,103 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Snowly - Snowplow Request Validator</title>
5
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
6
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
7
+ </head>
8
+ <body>
9
+ <div class="container">
10
+ <div class="header clearfix">
11
+ <nav>
12
+ <ul class="nav nav-pills pull-right">
13
+ <li role="presentation"><a href="https://github.com/angelim/snowly">Github</a></li>
14
+ <li role="presentation"><a href="https://github.com/snowplow/snowplow">Snowplow</a></li>
15
+ </ul>
16
+ </nav>
17
+ <h3 class="text-muted">Snowly - Snowplow Request Validator</h3>
18
+ </div>
19
+
20
+ <div class="jumbotron">
21
+ <p class="lead">Test your snowplow implementation locally!</p>
22
+ <p><strong>Snowly</strong> is a minimal collector implementation intended to validate your event tracking requests before emitting them to cloudfront or a closure collector.</p>
23
+ <p>When <strong>Snowly</strong> finds something wrong, it renders the parsed request along with its errors.</p>
24
+ <p>If everything is ok, Snowly delivers the default Snowplow pixel, unless you're using the debug mode.</p>
25
+ <p>Point your collector URL to <code><%= url %>i</code> and have fun!</p>
26
+ <p>
27
+ <a class="btn btn-lg btn-success" href="/i?&e=pv&page=Root%20README&url=http%3A%2F%2Fgithub.com%2Fsnowplow%2Fsnowplow&aid=snowplow&p=web&tv=no-js-0.1.0&ua=firefox&&eid=u2i3&debug=true" role="button">See it working!</a>
28
+ <a class="btn btn-lg btn-warning" href="/i?&e=pv&page=Root%20README&url=http%3A%2F%2Fgithub.com%2Fsnowplow%2Fsnowplow&aid=snowplow&p=i&tv=no-js-0.1.0&debug=true" role="button">Event with errors!</a>
29
+ </p>
30
+ <% unless Snowly.local_iglu_resolver_path %>
31
+ <div class="alert alert-danger" role="alert">The Local Iglu Resolver Path is missing.</div>
32
+ <% end %>
33
+ </div>
34
+
35
+ <div class="row marketing">
36
+ <div class="col-lg-12">
37
+ <div class="panel panel-default">
38
+ <div class="panel-heading">Current Configuration</div>
39
+ <table class="table">
40
+ <thead>
41
+ <tr>
42
+ <th>Environment Variable</th>
43
+ <th>Value</th>
44
+ <th>Description</th>
45
+ </tr>
46
+ </thead>
47
+ <tbody>
48
+ <tr>
49
+ <td>SNOWLY_DEBUG_MODE</td>
50
+ <td><%= Snowly.debug_mode %></td>
51
+ <td>Renders parsed request instead of a pixel. Defaults to false</td>
52
+ </tr>
53
+ <tr>
54
+ <td>LOCAL_IGLU_RESOLVER_PATH</td>
55
+ <td><%= Snowly.local_iglu_resolver_path %></td>
56
+ <td>Local path for contexts and unstructured event schemas.</td>
57
+ </tr>
58
+ </tbody>
59
+
60
+ </table>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ <div class="row marketing">
65
+ <div class="col-lg-6">
66
+ <div class="panel panel-default">
67
+ <div class="panel-heading">Resolved Schemas</div>
68
+ <div class="panel-body">
69
+ <% if @resolved_schemas and not @resolved_schemas == [] %>
70
+ <ul>
71
+ <% @resolved_schemas.each do |r| %>
72
+ <li><%= r %></li>
73
+ <% end %>
74
+ </ul>
75
+ <% else %>
76
+ <p>No resolved schemas</p>
77
+ <% end %>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ <div class="col-lg-6">
82
+ <h4>Local Iglu Resolver</h4>
83
+ <p>
84
+ Snowly must be able to find your custom context and unstructured event schemas.
85
+ Just like the Resolver you may have already configured for the official ETL tools, Snowly needs a
86
+ local path to find your custom schemas. You can store them under any path(eg: ~/schemas)
87
+ Inside that folder you must create a resolver compatible structure:
88
+ <code>~/schemas/com.yoursite/schema/my_context/1-0-0</code><br>
89
+ <code>~/schemas/com.yoursite/schema/my_event/1-0-0</code><br>
90
+ 1-0-0 is the file holding the schema.
91
+ </p>
92
+ <p>
93
+ When you emmit events, use the schema path from the <code>Resolver path</code><br/>
94
+ <code>{ schema: 'iglu:com.yoursite/schema/my_context/1-0-0', data: !some_schema_data! }</code>
95
+ </p>
96
+ <p>
97
+ Be sure to give your schemas an <a href="http://spacetelescope.github.io/understanding-json-schema/structuring.html#the-id-property">id</a>, so Snowly can output more helpful validation error messages.
98
+ </p>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </body>
103
+ </html>
@@ -0,0 +1,66 @@
1
+ # Extended validation to require custom dependencies
2
+ # This validation allows the schema designer to define requirements based on other attribute content.
3
+ # @example Require apartment number if address type is `apartment`
4
+ # {
5
+ # 'address': { 'type': string' },
6
+ # 'adress_type': { 'type': 'string', 'enum': ['house', 'apartment'] },
7
+ # 'apartment_number': { 'type': 'number'},
8
+ # 'custom_dependencies': {
9
+ # apartment_number: { 'address_type': 'apartment' }
10
+ # }
11
+ # }
12
+ # In this example if the address type is 'house', apartment_number if not required.
13
+ # It only becomes a requirement if address type is set to 'apartment'.
14
+ require 'json-schema/attribute'
15
+
16
+ class CustomDependenciesAttribute < JSON::Schema::Attribute
17
+ def self.validate(current_schema, data, fragments, processor, validator, options = {})
18
+ return unless data.is_a?(Hash)
19
+ current_schema.schema['custom_dependencies'].each do |property, dependency_value|
20
+ next unless accept_value?(dependency_value)
21
+ case dependency_value
22
+ when Array
23
+ dependency_value.each do |dependency_hash|
24
+ validate_dependency(current_schema, data, property, dependency_hash, fragments, processor, self, options)
25
+ end
26
+ when Hash
27
+ validate_dependency(current_schema, data, property, dependency_value, fragments, processor, self, options)
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.validate_dependency(schema, data, property, dependency_hash, fragments, processor, attribute, options)
33
+ key, value = Array(dependency_hash).flatten
34
+ return unless data[key.to_s] == value.to_s
35
+ return if data.has_key?(property.to_s)
36
+ message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}' when property '#{key}' is '#{value}'"
37
+ validation_error(processor, message, fragments, schema, attribute, options[:record_errors])
38
+ end
39
+
40
+ def self.accept_value?(value)
41
+ value.is_a?(Array) || value.is_a?(Hash)
42
+ end
43
+ end
44
+
45
+ # Registers custom dependencies for Draft-4. This used when evaluating the protocol schema
46
+ # snowly/schemas/snowplow_protocol.json
47
+ class RootExtendedSchema < JSON::Schema::Validator
48
+ def initialize
49
+ super
50
+ extend_schema_definition("http://json-schema.org/draft-04/schema#")
51
+ @attributes["custom_dependencies"] = CustomDependenciesAttribute
52
+ @uri = URI.parse("http://json-schema.org/draft-04/schema")
53
+ end
54
+ JSON::Validator.register_validator(self.new)
55
+ end
56
+
57
+ # Registers custom dependencies for Snowplot Self-Describing schema. Contexts and Unstructure events derive from it.
58
+ class DescExtendedSchema < JSON::Schema::Validator
59
+ def initialize
60
+ super
61
+ extend_schema_definition("http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#")
62
+ @attributes["custom_dependencies"] = CustomDependenciesAttribute
63
+ @uri = URI.parse("http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#")
64
+ end
65
+ JSON::Validator.register_validator(self.new)
66
+ end
@@ -0,0 +1,28 @@
1
+ require 'snowly/transformer'
2
+ module Snowly
3
+ class Request
4
+ attr_reader :query_string
5
+ def initialize(query_string)
6
+ @query_string = query_string
7
+ end
8
+
9
+ # Retuns request as json, after transforming parameters into column names
10
+ # @return [String] encoded JSON
11
+ def as_json
12
+ @json ||= as_hash.to_json
13
+ end
14
+
15
+ # Retuns request as hash, after transforming parameters into column names
16
+ # @return [Hash]
17
+ def as_hash
18
+ @hash ||= Transformer.transform(parsed_query)
19
+ end
20
+
21
+ # Returns query parameters as hash
22
+ # @return [Hash]
23
+ def parsed_query
24
+ @parsed_query ||= Rack::Utils.parse_nested_query(query_string)
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,58 @@
1
+ # Caches schemas found during validation so they don't have to be
2
+ # retrieved a second time. Also uses the resolvers convert the iglu: location to an actual address (local or remote)/
3
+ require 'singleton'
4
+ module Snowly
5
+ class SchemaCache
6
+ include Singleton
7
+ SNOWPLOW_IGLU_RESOLVER = 'http://iglucentral.com/schemas/'
8
+ @@schema_cache = {}
9
+
10
+ # Provides easy access to the schema cache based on its registered key
11
+ # @param location [String] Location provided in the schema
12
+ # @return [String] Json for schema
13
+ def [](location)
14
+ @@schema_cache[location] || save_in_cache(location)
15
+ end
16
+
17
+ # Resets the schema cache
18
+ def reset_cache
19
+ @@schema_cache = {}
20
+ end
21
+
22
+ # Accessor to the global cache
23
+ def cache
24
+ @@schema_cache
25
+ end
26
+
27
+ private
28
+
29
+ # If schema should be resolved to snowplow iglu server
30
+ # @param location [String]
31
+ # @return [true, false]
32
+ def from_snowplow?(location)
33
+ location['iglu:com.snowplowanalytics.snowplow']
34
+ end
35
+
36
+ # Translate an iglu address to an actual local or remote location
37
+ # @param location [String]
38
+ # @param resolver [String] local or remote path to look for the schema
39
+ # @return [String] Schema's actual location
40
+ def resolve(location, resolver)
41
+ location.sub(/^iglu\:/, resolver)
42
+ end
43
+
44
+ # Caches the schema content under its original location name
45
+ # @param location [String]
46
+ # @return [String] schema content
47
+ def save_in_cache(location)
48
+ content = if from_snowplow?(location)
49
+ uri = URI(resolve(location, SNOWPLOW_IGLU_RESOLVER))
50
+ Net::HTTP.get(uri)
51
+ else
52
+ File.read(resolve(location, Snowly.local_iglu_resolver_path))
53
+ end
54
+ @@schema_cache[location] = content
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,117 @@
1
+ # Maps query string parameters to column names to provide more helpful references on validations.
2
+ module Snowly
3
+ module Transformer
4
+ module_function
5
+ # Boolean fields are mapped to string because the tracker sends '1' or '0' in the query string.
6
+ # The actual conversion to boolean happens during the enrichment phase.
7
+ MAP = {
8
+ "e" => { field: "event", type: 'string' },
9
+ "ip" => { field: "user_ipaddress", type: "string" },
10
+ "aid" => { field: "app_id", type: "string" },
11
+ "p" => { field: "platform", type: "string" },
12
+ "tid" => { field: "txn_id", type: "integer" },
13
+ "uid" => { field: "user_id", type: "string" },
14
+ "duid" => { field: "domain_userid", type: "string" },
15
+ "nuid" => { field: "network_userid", type: "string" },
16
+ "ua" => { field: "useragent", type: "string" },
17
+ "fp" => { field: "user_fingerprint", type: "integer" },
18
+ "vid" => { field: "domain_sessionidx", type: "integer" },
19
+ "sid" => { field: "domain_sessionid", type: "string" },
20
+ "dtm" => { field: "dvce_created_tstamp", type: "integer" },
21
+ "ttm" => { field: "true_tstamp", type: "integer" },
22
+ "stm" => { field: "dvce_sent_tstamp", type: "integer" },
23
+ "tna" => { field: "name_tracker", type: "string" },
24
+ "tv" => { field: "v_tracker", type: "string" },
25
+ "cv" => { field: "v_collector", type: "string" },
26
+ "lang" => { field: "br_lang", type: "string" },
27
+ "f_pdf" => { field: "br_features_pdf", type: "string" },
28
+ "f_fla" => { field: "br_features_flash", type: "string" },
29
+ "f_java" => { field: "br_features_java", type: "string" },
30
+ "f_dir" => { field: "br_features_director", type: "string" },
31
+ "f_qt" => { field: "br_features_quicktime", type: "string" },
32
+ "f_realp" => { field: "br_features_realplayer", type: "string" },
33
+ "f_wma" => { field: "br_features_windowsmedia", type: "string" },
34
+ "f_gears" => { field: "br_features_gears", type: "string" },
35
+ "f_ag" => { field: "br_features_silverlight", type: "string" },
36
+ "cookie" => { field: "br_cookies", type: "string" },
37
+ "res" => { field: "screen_res_width_x_height", type: "string" },
38
+ "cd" => { field: "br_colordepth", type: "string" },
39
+ "tz" => { field: "os_timezone", type: "string" },
40
+ "refr" => { field: "page_referrer", type: "string" },
41
+ "url" => { field: "page_url", type: "string" },
42
+ "page" => { field: "page_title", type: "string" },
43
+ "cs" => { field: "doc_charset", type: "string" },
44
+ "ds" => { field: "doc_width_x_height", type: "string" },
45
+ "vp" => { field: "browser_viewport_width_x_height", type: "string" },
46
+ "eid" => { field: "event_id", type: "string" },
47
+ "co" => { field: "contexts", type: "json" },
48
+ "cx" => { field: "contexts", type: "base64" },
49
+ "ev_ca" => { field: "se_category", type: "string" },
50
+ "ev_ac" => { field: "se_action", type: "string" },
51
+ "ev_la" => { field: "se_label", type: "string" },
52
+ "ev_pr" => { field: "se_property", type: "string" },
53
+ "ev_va" => { field: "se_value", type: "string" },
54
+ "se_ca" => { field: "se_category", type: "string" },
55
+ "se_ac" => { field: "se_action", type: "string" },
56
+ "se_la" => { field: "se_label", type: "string" },
57
+ "se_pr" => { field: "se_property", type: "string" },
58
+ "se_va" => { field: "se_value", type: "number" },
59
+ "ue_pr" => { field: "unstruct_event", type: "json" },
60
+ "ue_px" => { field: "unstruct_event", type: "base64" },
61
+ "tr_id" => { field: "tr_orderid", type: "string" },
62
+ "tr_af" => { field: "tr_affiliation", type: "string" },
63
+ "tr_tt" => { field: "tr_total", type: "number" },
64
+ "tr_tx" => { field: "tr_tax", type: "number" },
65
+ "tr_sh" => { field: "tr_shipping", type: "number" },
66
+ "tr_ci" => { field: "tr_city", type: "string" },
67
+ "tr_st" => { field: "tr_state", type: "string" },
68
+ "tr_co" => { field: "tr_country", type: "string" },
69
+ "ti_id" => { field: "ti_orderid", type: "string" },
70
+ "ti_sk" => { field: "ti_sku", type: "string" },
71
+ "ti_na" => { field: "ti_name", type: "string" },
72
+ "ti_nm" => { field: "ti_name", type: "string" },
73
+ "ti_ca" => { field: "ti_category", type: "string" },
74
+ "ti_pr" => { field: "ti_price", type: "number" },
75
+ "ti_qu" => { field: "ti_quantity", type: "integer" },
76
+ "pp_mix" => { field: "pp_xoffset_min", type: "integer" },
77
+ "pp_max" => { field: "pp_xoffset_max", type: "integer" },
78
+ "pp_miy" => { field: "pp_yoffset_min", type: "integer" },
79
+ "pp_may" => { field: "pp_yoffset_max", type: "integer" },
80
+ "tr_cu" => { field: "tr_currency", type: "string" },
81
+ "ti_cu" => { field: "ti_currency", type: "integer" }
82
+ }
83
+
84
+ # Transforms the request params into column names
85
+ # @param parsed_query [Hash] hash using parameter names for keys
86
+ # @return [Hash] hash using column names for keys
87
+ def transform(parsed_query)
88
+ parsed_query.inject({}) do |all, (key, value)|
89
+ if node = MAP[key]
90
+ field = node[:field]
91
+ all[field] = convert(value, node[:type])
92
+ end
93
+ all
94
+ end
95
+ end
96
+
97
+ # Tries to cast or parse each value so they can be properly validated by json-schema
98
+ # If the casting fails, leaves the value as string and it will be caught by the valication
99
+ # @param value [String]
100
+ # @param type [String] the intended param type
101
+ def convert(value, type)
102
+ begin
103
+ case type
104
+ when 'json' then JSON.parse(value)
105
+ when 'base64' then JSON.parse(Base64.urlsafe_decode64(value))
106
+ when 'integer' then Integer(value)
107
+ when 'number' then Float(value)
108
+ else
109
+ value.to_s
110
+ end
111
+ rescue ArgumentError
112
+ value.to_s
113
+ end
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,95 @@
1
+ # Performs the validation for the root attributes and associated contexts and unstructured events.
2
+ require 'snowly/request'
3
+ require 'snowly/extensions/custom_dependencies'
4
+ module Snowly
5
+ class Validator
6
+ attr_reader :request, :errors
7
+
8
+ def initialize(query_string)
9
+ @request = Request.new query_string
10
+ @errors = []
11
+ end
12
+
13
+ # Loads the protocol schema created to describe snowplow events table attributes
14
+ # @return [Hash] parsed schema
15
+ def protocol_schema
16
+ @protocol_schema ||= JSON.parse File.read("lib/schemas/snowplow_protocol.json")
17
+ end
18
+
19
+ # @return [Hash] all contexts content and schema definitions
20
+ def associated_contexts
21
+ load_contexts request.as_hash['contexts']
22
+ end
23
+
24
+ # @return [Hash] all unstructured events content and schema definitions
25
+ def associated_unstruct_event
26
+ load_unstruct_event request.as_hash['unstruct_event']
27
+ end
28
+
29
+ # @return [Array<Hash>] all associated content
30
+ def associated_elements
31
+ (Array(associated_contexts) + Array(associated_unstruct_event)).compact
32
+ end
33
+
34
+ # Performs initial validation for associated contexts and loads their contents and definitions.
35
+ # @return [Array<Hash>]
36
+ def load_contexts(hash)
37
+ return unless hash
38
+ response = []
39
+ unless hash['data']
40
+ @errors << "All custom contexts must be contain a `data` element" and return
41
+ end
42
+ response << { content: hash['data'], definition: SchemaCache.instance[hash['schema']] }
43
+ unless hash['data'].is_a? Array
44
+ @errors << "All custom contexts must be wrapped in an Array" and return
45
+ end
46
+ hash['data'].each do |data_item|
47
+ response << { content: data_item['data'], definition: SchemaCache.instance[data_item['schema']] }
48
+ end
49
+ response
50
+ end
51
+
52
+ # Performs initial validation for associated unstructured events and loads their contents and definitions.
53
+ # @return [Array<Hash>]
54
+ def load_unstruct_event(hash)
55
+ return unless hash
56
+ response = []
57
+ unless hash['data']
58
+ @errors << "All custom unstruct event must be contain a `data` element" and return
59
+ end
60
+ outer_data = hash['data']
61
+ inner_data = outer_data['data']
62
+ response << { content: outer_data, definition: SchemaCache.instance[hash['schema']] }
63
+ response << { content: inner_data, definition: SchemaCache.instance[outer_data['schema']] }
64
+ response
65
+ end
66
+
67
+ # Validates associated contexts and unstructured events
68
+ def validate_associated
69
+ return unless associated_elements
70
+ associated_elements.each do |schema|
71
+ this_error = JSON::Validator.fully_validate JSON.parse(schema[:definition]), schema[:content]
72
+ @errors += this_error if this_error.count > 0
73
+ end
74
+ end
75
+
76
+ # Validates root attributes for the events table
77
+ def validate_root
78
+ this_error = JSON::Validator.fully_validate protocol_schema, request.as_hash
79
+ @errors += this_error if this_error.count > 0
80
+ end
81
+
82
+ # If request is valid
83
+ # @return [true, false] if valid
84
+ def valid?
85
+ @errors == []
86
+ end
87
+
88
+ # Entry point for validation.
89
+ def validate
90
+ validate_root
91
+ validate_associated
92
+ valid?
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,16 @@
1
+ # Register a validator for the self-describing schema.
2
+ # This is required to allow extended validations being attached to it.
3
+ require 'json-schema/validators/draft4'
4
+ module JSON
5
+ class Schema
6
+ class SelfDesc < Draft4
7
+ URL = "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#"
8
+ def initialize
9
+ super
10
+ @uri = JSON::Util::URI.parse(URL)
11
+ end
12
+
13
+ JSON::Validator.register_validator(self.new)
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
1
  module Snowly
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/snowly.rb CHANGED
@@ -1,5 +1,21 @@
1
+ require 'json-schema'
2
+ require 'snowly/validators/self_desc'
3
+ require "pry"
1
4
  require "snowly/version"
5
+ require 'json'
6
+ require 'rack'
7
+ require 'snowly/validator'
8
+ require 'snowly/schema_cache'
9
+ require 'active_support'
2
10
 
3
11
  module Snowly
4
- # Your code goes here...
12
+ mattr_accessor :local_iglu_resolver_path, :debug_mode
13
+
14
+ @@local_iglu_resolver_path = ENV['LOCAL_IGLU_RESOLVER_PATH']
15
+ @@debug_mode = ENV['SNOWLY_DEBUG_MODE'] || false
16
+
17
+ def self.config
18
+ yield self
19
+ end
5
20
  end
21
+
data/snowly.gemspec CHANGED
@@ -9,20 +9,29 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Alexandre Angelim"]
10
10
  spec.email = ["angelim@angelim.com.br"]
11
11
 
12
- spec.summary = %q{Snowplow Emitter Validator}
13
- spec.description = %q{Simple tester for snowplow event emitions. Validates core, unstructured events and contexts JSON schemas}
12
+ spec.summary = %q{Snowplow Request Validator}
13
+ spec.description = %q{Snowly is a minimal collector implementation intended to validate your event tracking requests before emitting them to cloudfront or a closure collector.}
14
14
  spec.homepage = "https://github.com/angelim/snowly"
15
+ spec.license = "MIT"
15
16
 
16
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
18
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.bindir = "bin"
19
+ spec.executables << 'snowly'
19
20
  spec.require_paths = ["lib"]
21
+
20
22
 
21
- spec.add_dependency 'json-schema', '~> 2.6.2'
23
+ spec.add_dependency 'json-schema', '~> 2.6'
24
+ spec.add_dependency 'rack', '~> 1.6'
25
+ spec.add_dependency 'activesupport', "~> 3.0"
26
+ spec.add_dependency 'sinatra', '~> 1.4'
27
+ spec.add_dependency 'sinatra-contrib', '~> 1.4'
28
+ spec.add_dependency 'vegas', '~> 0.1'
22
29
 
23
30
  spec.add_development_dependency 'bundler', '~> 1.11'
24
31
  spec.add_development_dependency 'rake', '~> 10.0'
25
32
  spec.add_development_dependency 'rspec', '~> 3.0'
26
- spec.add_development_dependency 'pry'
27
- spec.add_development_dependency 'snowplow-tracker', '~> 0.5.2'
33
+ spec.add_development_dependency 'pry-byebug', '~> 3.3'
34
+ spec.add_development_dependency 'snowplow-tracker', '~> 0.5'
35
+ spec.add_development_dependency 'webmock', '~> 2.0'
36
+ spec.add_development_dependency "shotgun", '~> 0.9'
28
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snowly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Angelim
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-20 00:00:00.000000000 Z
11
+ date: 2016-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json-schema
@@ -16,14 +16,84 @@ dependencies:
16
16
  requirements:
17
17
  - - ~>
18
18
  - !ruby/object:Gem::Version
19
- version: 2.6.2
19
+ version: '2.6'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ~>
25
25
  - !ruby/object:Gem::Version
26
- version: 2.6.2
26
+ version: '2.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sinatra-contrib
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '1.4'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '1.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: vegas
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '0.1'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '0.1'
27
97
  - !ruby/object:Gem::Dependency
28
98
  name: bundler
29
99
  requirement: !ruby/object:Gem::Requirement
@@ -67,38 +137,67 @@ dependencies:
67
137
  - !ruby/object:Gem::Version
68
138
  version: '3.0'
69
139
  - !ruby/object:Gem::Dependency
70
- name: pry
140
+ name: pry-byebug
71
141
  requirement: !ruby/object:Gem::Requirement
72
142
  requirements:
73
- - - ! '>='
143
+ - - ~>
74
144
  - !ruby/object:Gem::Version
75
- version: '0'
145
+ version: '3.3'
76
146
  type: :development
77
147
  prerelease: false
78
148
  version_requirements: !ruby/object:Gem::Requirement
79
149
  requirements:
80
- - - ! '>='
150
+ - - ~>
81
151
  - !ruby/object:Gem::Version
82
- version: '0'
152
+ version: '3.3'
83
153
  - !ruby/object:Gem::Dependency
84
154
  name: snowplow-tracker
85
155
  requirement: !ruby/object:Gem::Requirement
86
156
  requirements:
87
157
  - - ~>
88
158
  - !ruby/object:Gem::Version
89
- version: 0.5.2
159
+ version: '0.5'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ~>
165
+ - !ruby/object:Gem::Version
166
+ version: '0.5'
167
+ - !ruby/object:Gem::Dependency
168
+ name: webmock
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ~>
172
+ - !ruby/object:Gem::Version
173
+ version: '2.0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ~>
179
+ - !ruby/object:Gem::Version
180
+ version: '2.0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: shotgun
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ~>
186
+ - !ruby/object:Gem::Version
187
+ version: '0.9'
90
188
  type: :development
91
189
  prerelease: false
92
190
  version_requirements: !ruby/object:Gem::Requirement
93
191
  requirements:
94
192
  - - ~>
95
193
  - !ruby/object:Gem::Version
96
- version: 0.5.2
97
- description: Simple tester for snowplow event emitions. Validates core, unstructured
98
- events and contexts JSON schemas
194
+ version: '0.9'
195
+ description: Snowly is a minimal collector implementation intended to validate your
196
+ event tracking requests before emitting them to cloudfront or a closure collector.
99
197
  email:
100
198
  - angelim@angelim.com.br
101
- executables: []
199
+ executables:
200
+ - snowly
102
201
  extensions: []
103
202
  extra_rdoc_files: []
104
203
  files:
@@ -110,11 +209,23 @@ files:
110
209
  - Rakefile
111
210
  - bin/console
112
211
  - bin/setup
212
+ - bin/snowly
213
+ - config.ru
214
+ - lib/schemas/snowplow_protocol.json
113
215
  - lib/snowly.rb
216
+ - lib/snowly/app/collector.rb
217
+ - lib/snowly/app/views/index.erb
218
+ - lib/snowly/extensions/custom_dependencies.rb
219
+ - lib/snowly/request.rb
220
+ - lib/snowly/schema_cache.rb
221
+ - lib/snowly/transformer.rb
222
+ - lib/snowly/validator.rb
223
+ - lib/snowly/validators/self_desc.rb
114
224
  - lib/snowly/version.rb
115
225
  - snowly.gemspec
116
226
  homepage: https://github.com/angelim/snowly
117
- licenses: []
227
+ licenses:
228
+ - MIT
118
229
  metadata: {}
119
230
  post_install_message:
120
231
  rdoc_options: []
@@ -135,6 +246,6 @@ rubyforge_project:
135
246
  rubygems_version: 2.2.2
136
247
  signing_key:
137
248
  specification_version: 4
138
- summary: Snowplow Emitter Validator
249
+ summary: Snowplow Request Validator
139
250
  test_files: []
140
251
  has_rdoc: