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 +4 -4
- data/.gitignore +1 -0
- data/bin/snowly +10 -0
- data/config.ru +6 -0
- data/lib/schemas/snowplow_protocol.json +265 -0
- data/lib/snowly/app/collector.rb +42 -0
- data/lib/snowly/app/views/index.erb +103 -0
- data/lib/snowly/extensions/custom_dependencies.rb +66 -0
- data/lib/snowly/request.rb +28 -0
- data/lib/snowly/schema_cache.rb +58 -0
- data/lib/snowly/transformer.rb +117 -0
- data/lib/snowly/validator.rb +95 -0
- data/lib/snowly/validators/self_desc.rb +16 -0
- data/lib/snowly/version.rb +1 -1
- data/lib/snowly.rb +17 -1
- data/snowly.gemspec +16 -7
- metadata +128 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d41577c932e8b4ba8f150d57bfe887f4be951c6c
|
4
|
+
data.tar.gz: 84a1422730f60bb6134ff28a6e6310a699fcd9e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 749b0bb7137ff4782bece6bde1dfdc8fd5ed18db9002b0f9f28e9c4e5f186da137b91c448b8d3b289e638108da3e7d8117eea96487231abb7583ed940a670bbd
|
7
|
+
data.tar.gz: 151aff343e3031e30158aebe463fb079b94d2fa05eba04a9f8495659e5d30462417105197a4b17af14d57c9ad9314bf1a9260b395f954b36c66566fde78ab43a
|
data/.gitignore
CHANGED
data/bin/snowly
ADDED
data/config.ru
ADDED
@@ -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
|
data/lib/snowly/version.rb
CHANGED
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
|
-
|
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
|
13
|
-
spec.description = %q{
|
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 = "
|
18
|
-
spec.executables
|
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
|
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
|
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.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandre Angelim
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
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
|
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
|
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: '
|
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: '
|
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
|
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.
|
97
|
-
description:
|
98
|
-
|
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
|
249
|
+
summary: Snowplow Request Validator
|
139
250
|
test_files: []
|
140
251
|
has_rdoc:
|