squash_javascript 1.0.0

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.
@@ -0,0 +1,181 @@
1
+ # Copyright 2012 Square Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'json'
16
+
17
+ # An in-memory representation of a JavaScript source map.
18
+
19
+ class Squash::Javascript::SourceMap
20
+ # @private
21
+ VLQ_CONTINUATION_BIT = 1 << 5
22
+
23
+ # @return [Array<Squash::Javascript::SourceMap::Mapping>] The individual
24
+ # mappings in this source map.
25
+ attr_reader :mappings
26
+
27
+ # @private
28
+ def initialize(entries=[])
29
+ @mappings = entries
30
+ sort!
31
+ end
32
+
33
+ # Parses a JSON version-3 source map file and generates a SourceMap from it.
34
+ #
35
+ # @param [String] source_file A path to a source map file.
36
+ # @param [String] route The URL where the compiled JavaScript asset is
37
+ # accessed.
38
+ # @param [Hash] options Additional options.
39
+ # @option options [String] :root If set, overrides the project root specified
40
+ # in the `sourceRoot` field with this root. Absolute paths will be converted
41
+ # into project-local paths (suitable for Git-blaming) using this root.
42
+
43
+ def self.from_sourcemap(source_file, route, options={})
44
+ fields = JSON.parse(File.read(source_file))
45
+ raise "Only version 3 source maps are supported" unless fields['version'] == 3
46
+
47
+ project_root = options[:root] || fields['sourceRoot'] or raise("Must specify a project root in the source map or method options")
48
+ project_root << '/' unless project_root[-2..-1] == '/'
49
+
50
+ route = route
51
+ sources = fields['sources'].map { |source| source.sub(/^#{Regexp.escape project_root}/, '') }
52
+ names = fields['names']
53
+
54
+ mappings = fields['mappings'].split(';').map { |group| group.split ',' }
55
+ entries = Array.new
56
+
57
+ source_file_counter = 0
58
+ source_line_counter = 0
59
+ source_column_counter = 0
60
+ symbol_counter = 0
61
+ mappings.each_with_index do |group, compiled_line_number|
62
+ compiled_column_counter = 0
63
+ group.each do |segment|
64
+ values = decode64vlq(segment)
65
+ compiled_column_counter += values[0]
66
+ source_file_counter += values[1] if values[1]
67
+ source_line_counter += values[2] if values[2]
68
+ source_column_counter += values[3] if values[3]
69
+ symbol_counter += values[4] if values[4]
70
+ entries << Mapping.new(
71
+ route, compiled_line_number, compiled_column_counter,
72
+ sources[source_file_counter], source_line_counter, source_column_counter,
73
+ names[symbol_counter]
74
+ )
75
+ end
76
+ end
77
+
78
+ return new(entries)
79
+ end
80
+
81
+ # @private
82
+ def <<(obj)
83
+ @mappings << obj
84
+ sort!
85
+ end
86
+
87
+ # Given the URL of a minified JavaScript asset, and a line and column number,
88
+ # attempts to locate the original source file and line number.
89
+ #
90
+ # @param [String] route The URL of the JavaScript file.
91
+ # @param [Fixnum] line The line number.
92
+ # @param [Fixnum] column The character number within the line.
93
+
94
+ def resolve(route, line, column)
95
+ index = 0
96
+ mapping = nil
97
+ while index < mappings.length
98
+ entry = mappings[index]
99
+ mapping = entry if entry.route == route && entry.compiled_line == line && entry.compiled_column <= column
100
+ break if entry.route > route || entry.compiled_line > line
101
+ index += 1
102
+ end
103
+
104
+ return mapping
105
+ end
106
+
107
+ private
108
+
109
+ def sort!
110
+ @mappings.sort_by! { |m| [m.route, m.compiled_line, m.compiled_column] }
111
+ end
112
+
113
+ def self.decode64vlq(string)
114
+ bytes = string.bytes.to_a
115
+ byte_index = 0
116
+ result = []
117
+
118
+ begin
119
+ raise "Base-64 VLQ-encoded string unexpectedly terminated" if byte_index >= string.bytesize
120
+
121
+ number = 0
122
+ continuation = false
123
+ shift = 0
124
+ begin
125
+ # each character corresponds to a 6-bit word
126
+ digit = decode64(bytes[byte_index])
127
+ byte_index += 1
128
+ # the most significant bit is the continuation bit
129
+ continuation = (digit & VLQ_CONTINUATION_BIT) != 0
130
+ # the remaining bits are the number
131
+ digit = digit & (VLQ_CONTINUATION_BIT - 1)
132
+ # continuations are little-endian
133
+ number += (digit << shift)
134
+ shift += 5
135
+ end while continuation
136
+
137
+ # the LSB is the sign bit
138
+ number = (number & 1 > 0) ? -(number >> 1) : (number >> 1)
139
+ result << number
140
+ end while byte_index < string.bytesize
141
+
142
+ return result
143
+ end
144
+
145
+ def self.decode64(byte)
146
+ case byte
147
+ when 65..90 then byte - 65 # A..Z => 0..25
148
+ when 97..122 then byte - 71 # a..z => 26..51
149
+ when 48..57 then byte + 4 # 0..9 => 52..61
150
+ when 43 then 62 # + => 62
151
+ when 47 then 63 # / => 63
152
+ else raise "Invalid byte #{byte} in base-64 VLQ string"
153
+ end
154
+ end
155
+
156
+ class Mapping
157
+ attr_accessor :route, :compiled_line, :compiled_column, :source_file, :source_line, :source_column, :symbol
158
+
159
+ def self.from_json(obj)
160
+ new *obj
161
+ end
162
+
163
+ def initialize(route, compiled_line, compiled_column, source_file, source_line, source_column, symbol)
164
+ @route = route
165
+ @compiled_line = compiled_line
166
+ @compiled_column = compiled_column
167
+ @source_file = source_file
168
+ @source_line = source_line
169
+ @source_column = source_column
170
+ @symbol = symbol
171
+ end
172
+
173
+ def as_json(options=nil)
174
+ [route, compiled_line, compiled_column, source_file, source_line, source_column, symbol]
175
+ end
176
+
177
+ def to_json(options=nil)
178
+ as_json.to_json
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,26 @@
1
+ # Copyright 2012 Square Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Container module for classes relating to Squash.
16
+
17
+ module Squash
18
+
19
+ # Container module for classes relating to the JavaScript client library.
20
+
21
+ module Javascript
22
+ end
23
+ end
24
+
25
+ require 'squash/javascript/source_map'
26
+ require 'squash/javascript/engine'
@@ -0,0 +1,219 @@
1
+ # Copyright 2012 Square Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ root = exports ? this
16
+
17
+ # Singleton class for the client library. See the README file for usage
18
+ # examples. The singleton instance is accessed using the {.instance} method.
19
+ #
20
+ class root.SquashJavascript
21
+ _instance = undefined
22
+
23
+ # @return [_SquashJavascript] The singleton instance.
24
+ #
25
+ @instance: -> _instance ?= new _SquashJavascript()
26
+
27
+ # See {root.SquashJavascript}.
28
+ #
29
+ class _SquashJavascript
30
+ # @private
31
+ constructor: ->
32
+ TraceKit.report.subscribe (error) -> SquashJavascript.instance().report(error)
33
+
34
+ # Sets configuration options. See the README file for a list of accepted
35
+ # configuration options. Multiple calls will merge new options in.
36
+ #
37
+ # @param [Object] options New options to apply.
38
+ #
39
+ configure: (options) ->
40
+ @options ||= {
41
+ disabled: false
42
+ notifyPath: '/api/1.0/notify'
43
+ transmitTimeout: 15000
44
+ ignoredExceptionClasses: []
45
+ ignoredExceptionMessages: {}
46
+ }
47
+ for own key, value of options
48
+ @options[key] = value
49
+
50
+ # Notify Squash of an error. The client must be configured first. The error
51
+ # must have a valid stack trace (i.e., it must have been thrown). Does not
52
+ # re-throw the error; that's your responsibility.
53
+ #
54
+ # You shouldn't normally need to call this method. Squash automatically
55
+ # installs a listener that notifies the server for thrown exceptions.
56
+ #
57
+ # @param [Error] error The error to record.
58
+ #
59
+ notify: (error) ->
60
+ if error instanceof Error
61
+ TraceKit.report(error)
62
+ else
63
+ throw error
64
+
65
+ # @private
66
+ report: (error) ->
67
+ try
68
+ return false if @options?.disabled
69
+ if !@options?.APIKey || !@options?.environment ||
70
+ !@options?.revision || !@options?.APIHost
71
+ console.error "Missing required Squash configuration keys"
72
+ return false
73
+
74
+ return false if this.shouldIgnoreError(error)
75
+ return false unless error.stack
76
+
77
+ fields = arguments[1] || new Object()
78
+ fields.api_key = @options.APIKey
79
+ fields.environment = @options.environment
80
+ fields.client = "javascript"
81
+ fields.revision = @options.revision
82
+
83
+ fields.class_name = error.type || error.name
84
+ # errors that make it up to window.onerror get stupidly rewritten: their
85
+ # class is set to "Error" and the ACTUAL class is integrated into the
86
+ # message (e.g., "Uncaught TypeError: [message]")
87
+ if !error.name && (matches = error.message.match(/^(Uncaught )?(\w+): (.+)/))
88
+ fields.class_name = matches[2]
89
+ fields.message = matches[3]
90
+ else
91
+ fields.message = error.message
92
+ fields.class_name ?= 'Error' # when all else fails
93
+
94
+ fields.backtraces = buildBacktrace(error.stack)
95
+ fields.capture_method = error.mode
96
+ fields.occurred_at = ISODateString(new Date())
97
+
98
+ fields.schema = window.location.protocol.replace(/:$/, '')
99
+ fields.host = window.location.hostname
100
+ fields.port = window.location.port if window.location.port.length > 0
101
+ fields.path = window.location.pathname
102
+ fields.query = window.location.search
103
+ fields.fragment = window.location.hash if window.location.hash != ''
104
+
105
+ fields.user_agent = navigator.userAgent
106
+
107
+ fields.screen_width = screen.width
108
+ fields.screen_height = screen.height
109
+ fields.window_width = window.innerWidth
110
+ fields.window_height = window.innerHeight
111
+ fields.color_depth = screen.colorDepth
112
+
113
+ body = JSON.stringify(fields)
114
+ this.HTTPTransmit (@options.APIHost + @options.notifyPath),
115
+ [ ['Content-Type', 'application/json'] ],
116
+ body
117
+
118
+ return true
119
+ catch internal_error
120
+ console.error "Error while trying to notify Squash:", internal_error.stack
121
+ console.error "-- original error:", error
122
+
123
+ # Runs the given `block`. If an exception is thrown within the function, adds
124
+ # the given user data to the exception and re-throws it.
125
+ #
126
+ # @param [Object] data The user data to add to any error.
127
+ # @param [function] block The code to run.
128
+ # @return [Object] The return value of `block`.
129
+ # @see #addingUserData
130
+ #
131
+ addUserData: (data, block) ->
132
+ try
133
+ block()
134
+ catch err
135
+ err._squash_user_data ?= {}
136
+ mergeBang err._squash_user_data, data
137
+ throw err
138
+
139
+ # Wraps `block` in a call to {#addUserData} and returns it. Any arguments are
140
+ # passed through.
141
+ #
142
+ # @see #addUserData
143
+ #
144
+ addingUserData: (data, block) ->
145
+ (args...) -> SquashJavascript.instance().addUserData(data, -> block(args...))
146
+
147
+ # Runs the given `block`. If an exception is thrown within the function of one
148
+ # of the types given, does not report the exception to Squash. Re-throws _all_
149
+ # exceptions.
150
+ #
151
+ # @param [class..., String...] exceptions A list of error types to ignore.
152
+ # @param [function] block The code to run.
153
+ # @return [Object] The return value of `block`.
154
+ # @see #ignoringExceptions
155
+ #
156
+ ignoreExceptions: (exceptions..., block) ->
157
+ try
158
+ block()
159
+ catch err
160
+ err._squash_ignored_exceptions = (err._squash_ignored_exceptions || []).concat(exceptions)
161
+ throw err
162
+
163
+ # Wraps `block` in a call to {#ignoreExceptions} and returns it. Any arguments
164
+ # are passed through.
165
+ #
166
+ # @see #ignoreExceptions
167
+ #
168
+ ignoringExceptions: (exceptions..., block) ->
169
+ (args...) -> SquashJavascript.instance().ignoreExceptions(exceptions..., -> block(args...))
170
+
171
+ ################################## PRIVATES ##################################
172
+
173
+ # @private
174
+ HTTPTransmit: (url, headers, body) ->
175
+ request = new XMLHttpRequest()
176
+ request.timeout = @options.transmitTimeout
177
+ request.open "POST", url, true
178
+ for header in headers
179
+ request.setRequestHeader header[0], header[1]
180
+ request.send body
181
+ request
182
+
183
+ # @private
184
+ shouldIgnoreError: (error) ->
185
+ ignored_classes = @options.ignoredExceptionClasses.concat(error._squash_ignored_exceptions || [])
186
+
187
+ return true if any(ignored_classes, (klass) -> error.name == klass)
188
+
189
+ return any(@options.ignoredExceptionMessages, (class_name, messages) ->
190
+ if error.name == class_name
191
+ return any(messages, (message) -> error.message.match(message))
192
+ else
193
+ return false
194
+ )
195
+
196
+ buildBacktrace = (stack) ->
197
+ backtraces = []
198
+ for line in stack
199
+ context = line.context
200
+ context = null if context && any(context, (cline) -> cline.length > 200)
201
+ backtraces.push ['_JS_ASSET_', line.url, line.line, line.column, line.func, context]
202
+ return [ ["Active Thread", true, backtraces] ]
203
+
204
+ ISODateString = (d) ->
205
+ pad = (n) -> if n < 10 then '0' + n else n
206
+ "#{d.getUTCFullYear()}-#{pad(d.getUTCMonth() + 1)}-#{pad d.getUTCDate()}T#{pad d.getUTCHours()}:#{pad d.getUTCMinutes()}:#{pad d.getUTCSeconds()}Z"
207
+
208
+ any = (obj, condition) ->
209
+ if obj instanceof Array
210
+ for element in obj
211
+ return true if condition(element)
212
+ else
213
+ for own key, value of obj
214
+ return true if condition(key, value)
215
+ return false
216
+
217
+ mergeBang = (modified, constant) ->
218
+ for own key, value of constant
219
+ modified[key] = value
@@ -0,0 +1,16 @@
1
+ // Copyright 2012 Square Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ //= require ./tracekit
16
+ //= require ./client