snowly 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  !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: