inferno_core 0.0.7 → 0.1.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/inferno/apps/web/controllers/test_runs/create.rb +2 -1
- data/lib/inferno/apps/web/index.html.erb +1 -0
- data/lib/inferno/db/migrations/001_create_initial_structure.rb +54 -53
- data/lib/inferno/db/migrations/002_add_wait_support.rb +2 -2
- data/lib/inferno/db/migrations/003_update_session_data.rb +7 -7
- data/lib/inferno/db/migrations/004_add_request_results_table.rb +2 -2
- data/lib/inferno/db/migrations/005_add_updated_at_index_to_results.rb +1 -1
- data/lib/inferno/db/migrations/006_remove_unused_tables.rb +38 -0
- data/lib/inferno/db/schema.rb +16 -42
- data/lib/inferno/dsl/configurable.rb +12 -5
- data/lib/inferno/dsl/fhir_client.rb +62 -20
- data/lib/inferno/dsl/fhir_client_builder.rb +16 -0
- data/lib/inferno/dsl/fhir_validation.rb +104 -0
- data/lib/inferno/dsl/oauth_credentials.rb +119 -0
- data/lib/inferno/dsl/runnable.rb +20 -8
- data/lib/inferno/entities/request.rb +7 -1
- data/lib/inferno/exceptions.rb +19 -0
- data/lib/inferno/ext/fhir_client.rb +13 -0
- data/lib/inferno/public/217.bundle.js +1 -1
- data/lib/inferno/public/bundle.js +154 -1
- data/lib/inferno/public/bundle.js.LICENSE.txt +15 -0
- data/lib/inferno/repositories/session_data.rb +40 -6
- data/lib/inferno/spec_support.rb +1 -1
- data/lib/inferno/test_runner.rb +8 -3
- data/lib/inferno/utils/middleware/request_logger.rb +8 -2
- data/lib/inferno/version.rb +1 -1
- data/spec/factories/request.rb +1 -1
- data/spec/factories/result.rb +2 -2
- data/spec/fixtures/basic_test_group.rb +1 -0
- metadata +11 -8
@@ -1,38 +1,113 @@
|
|
1
1
|
module Inferno
|
2
2
|
module DSL
|
3
|
+
# This module contains the methods needed to configure a validator to
|
4
|
+
# perform validation of FHIR resources. The actual validation is performed
|
5
|
+
# by an external FHIR validation service. Tests will typically rely on
|
6
|
+
# `assert_valid_resource` for validation rather than directly calling
|
7
|
+
# methods on a validator.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
#
|
11
|
+
# validator do
|
12
|
+
# url 'http://example.com/validator'
|
13
|
+
# exclude_message { |message| message.type == 'info' }
|
14
|
+
# perform_additional_validation do |resource, profile_url|
|
15
|
+
# if something_is_wrong
|
16
|
+
# { type: 'error', message: 'something is wrong' }
|
17
|
+
# else
|
18
|
+
# { type: 'info', message: 'everything is ok' }
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
3
22
|
module FHIRValidation
|
4
23
|
def self.included(klass)
|
5
24
|
klass.extend ClassMethods
|
6
25
|
end
|
7
26
|
|
27
|
+
# Perform validation, and add validation messages to the runnable
|
28
|
+
#
|
29
|
+
# @param resource [FHIR::Model]
|
30
|
+
# @param profile_url [String]
|
31
|
+
# @param validator [Symbol] the name of the validator to use
|
32
|
+
# @return [Boolean] whether the resource is valid
|
8
33
|
def resource_is_valid?(resource: self.resource, profile_url: nil, validator: :default)
|
9
34
|
find_validator(validator).resource_is_valid?(resource, profile_url, self)
|
10
35
|
end
|
11
36
|
|
37
|
+
# Find a particular validator. Looks through a runnable's parents up to
|
38
|
+
# the suite to find a validator with a particular name
|
12
39
|
def find_validator(validator_name)
|
13
40
|
self.class.find_validator(validator_name)
|
14
41
|
end
|
15
42
|
|
16
43
|
class Validator
|
44
|
+
# @private
|
17
45
|
def initialize(&block)
|
18
46
|
instance_eval(&block)
|
19
47
|
end
|
20
48
|
|
49
|
+
# @private
|
21
50
|
def default_validator_url
|
22
51
|
ENV.fetch('VALIDATOR_URL')
|
23
52
|
end
|
24
53
|
|
54
|
+
# Set the url of the validator service
|
55
|
+
#
|
56
|
+
# @param url [String]
|
25
57
|
def url(validator_url = nil)
|
26
58
|
@url = validator_url if validator_url
|
27
59
|
|
28
60
|
@url
|
29
61
|
end
|
30
62
|
|
63
|
+
# @private
|
64
|
+
def additional_validations
|
65
|
+
@additional_validations ||= []
|
66
|
+
end
|
67
|
+
|
68
|
+
# Perform validation steps in addition to FHIR validation.
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# perform_additional_validation do |resource, profile_url|
|
72
|
+
# if something_is_wrong
|
73
|
+
# { type: 'error', message: 'something is wrong' }
|
74
|
+
# else
|
75
|
+
# { type: 'info', message: 'everything is ok' }
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
# @yieldparam resource [FHIR::Model] the resource being validated
|
79
|
+
# @yieldparam profile_url [String] the profile the resource is being
|
80
|
+
# validated against
|
81
|
+
# @yieldreturn [Array<Hash<Symbol, String>>,Hash<Symbol, String>] The
|
82
|
+
# block should return a Hash or an Array of Hashes if any validation
|
83
|
+
# messages should be added. The Hash must contain two keys: `:type`
|
84
|
+
# and `:message`. `:type` can have a value of `'info'`, `'warning'`,
|
85
|
+
# or `'error'`. A type of `'error'` means the resource is invalid.
|
86
|
+
# `:message` contains the message string itself.
|
87
|
+
def perform_additional_validation(&block)
|
88
|
+
additional_validations << block
|
89
|
+
end
|
90
|
+
|
91
|
+
# @private
|
92
|
+
def additional_validation_messages(resource, profile_url)
|
93
|
+
additional_validations
|
94
|
+
.flat_map { |step| step.call(resource, profile_url) }
|
95
|
+
.select { |message| message.is_a? Hash }
|
96
|
+
end
|
97
|
+
|
98
|
+
# Filter out unwanted validation messages
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# validator do
|
102
|
+
# exclude_message { |message| message.type == 'info' }
|
103
|
+
# end
|
104
|
+
# @yieldparam message [Inferno::Entities::Message]
|
31
105
|
def exclude_message(&block)
|
32
106
|
@exclude_message = block if block_given?
|
33
107
|
@exclude_message
|
34
108
|
end
|
35
109
|
|
110
|
+
# @see Inferno::DSL::FHIRValidation#resource_is_valid?
|
36
111
|
def resource_is_valid?(resource, profile_url, runnable)
|
37
112
|
profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url
|
38
113
|
|
@@ -40,6 +115,8 @@ module Inferno
|
|
40
115
|
|
41
116
|
message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue) } || []
|
42
117
|
|
118
|
+
message_hashes.concat(additional_validation_messages(resource, profile_url))
|
119
|
+
|
43
120
|
filter_messages(message_hashes)
|
44
121
|
|
45
122
|
message_hashes
|
@@ -47,10 +124,12 @@ module Inferno
|
|
47
124
|
.none? { |message_hash| message_hash[:type] == 'error' }
|
48
125
|
end
|
49
126
|
|
127
|
+
# @private
|
50
128
|
def filter_messages(message_hashes)
|
51
129
|
message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message
|
52
130
|
end
|
53
131
|
|
132
|
+
# @private
|
54
133
|
def message_hash_from_issue(issue)
|
55
134
|
{
|
56
135
|
type: issue_severity(issue),
|
@@ -58,6 +137,7 @@ module Inferno
|
|
58
137
|
}
|
59
138
|
end
|
60
139
|
|
140
|
+
# @private
|
61
141
|
def issue_severity(issue)
|
62
142
|
case issue.severity
|
63
143
|
when 'warning'
|
@@ -69,6 +149,7 @@ module Inferno
|
|
69
149
|
end
|
70
150
|
end
|
71
151
|
|
152
|
+
# @private
|
72
153
|
def issue_message(issue)
|
73
154
|
location = if issue.respond_to?(:expression)
|
74
155
|
issue.expression&.join(', ')
|
@@ -79,6 +160,11 @@ module Inferno
|
|
79
160
|
"#{location}: #{issue&.details&.text}"
|
80
161
|
end
|
81
162
|
|
163
|
+
# Post a resource to the validation service for validating.
|
164
|
+
#
|
165
|
+
# @param resource [FHIR::Model]
|
166
|
+
# @param profile_url [String]
|
167
|
+
# @return [String] the body of the validation response
|
82
168
|
def validate(resource, profile_url)
|
83
169
|
RestClient.post(
|
84
170
|
"#{url}/validate",
|
@@ -94,10 +180,28 @@ module Inferno
|
|
94
180
|
@fhir_validators ||= {}
|
95
181
|
end
|
96
182
|
|
183
|
+
# Define a validator
|
184
|
+
# @example
|
185
|
+
# validator do
|
186
|
+
# url 'http://example.com/validator'
|
187
|
+
# exclude_message { |message| message.type == 'info' }
|
188
|
+
# perform_additional_validation do |resource, profile_url|
|
189
|
+
# if something_is_wrong
|
190
|
+
# { type: 'error', message: 'something is wrong' }
|
191
|
+
# else
|
192
|
+
# { type: 'info', message: 'everything is ok' }
|
193
|
+
# end
|
194
|
+
# end
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# @param name [Symbol] the name of the validator, only needed if you are
|
198
|
+
# using multiple validators
|
97
199
|
def validator(name = :default, &block)
|
98
200
|
fhir_validators[name] = Inferno::DSL::FHIRValidation::Validator.new(&block)
|
99
201
|
end
|
100
202
|
|
203
|
+
# Find a particular validator. Looks through a runnable's parents up to
|
204
|
+
# the suite to find a validator with a particular name
|
101
205
|
def find_validator(validator_name)
|
102
206
|
validator = fhir_validators[validator_name] || parent&.find_validator(validator_name)
|
103
207
|
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require_relative '../entities/attributes'
|
2
|
+
|
3
|
+
module Inferno
|
4
|
+
module DSL
|
5
|
+
class OAuthCredentials
|
6
|
+
ATTRIBUTES = [
|
7
|
+
:access_token,
|
8
|
+
:refresh_token,
|
9
|
+
:token_url,
|
10
|
+
:client_id,
|
11
|
+
:client_secret,
|
12
|
+
:token_retrieval_time,
|
13
|
+
:expires_in,
|
14
|
+
:name
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
include Entities::Attributes
|
18
|
+
|
19
|
+
attr_accessor :client
|
20
|
+
|
21
|
+
def initialize(raw_attributes_hash)
|
22
|
+
attributes_hash = raw_attributes_hash.symbolize_keys
|
23
|
+
|
24
|
+
invalid_keys = attributes_hash.keys - ATTRIBUTES
|
25
|
+
|
26
|
+
raise Exceptions::UnknownAttributeException.new(invalid_keys, self.class) if invalid_keys.present?
|
27
|
+
|
28
|
+
attributes_hash.each do |name, value|
|
29
|
+
value = DateTime.parse(value) if name == :token_retrieval_time && value.is_a?(String)
|
30
|
+
|
31
|
+
instance_variable_set(:"@#{name}", value)
|
32
|
+
end
|
33
|
+
|
34
|
+
self.token_retrieval_time = DateTime.now if token_retrieval_time.blank?
|
35
|
+
end
|
36
|
+
|
37
|
+
# @api private
|
38
|
+
def to_hash
|
39
|
+
self.class::ATTRIBUTES.each_with_object({}) do |attribute, hash|
|
40
|
+
value = send(attribute)
|
41
|
+
next if value.nil?
|
42
|
+
|
43
|
+
value = token_retrieval_time.iso8601 if attribute == :token_retrieval_time
|
44
|
+
|
45
|
+
hash[attribute] = value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
def to_s
|
51
|
+
JSON.generate(to_hash)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def add_to_client(client)
|
56
|
+
client.oauth_credentials = self
|
57
|
+
self.client = client
|
58
|
+
|
59
|
+
return unless access_token.present?
|
60
|
+
|
61
|
+
client.set_bearer_token(access_token)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def need_to_refresh?
|
66
|
+
return false if access_token.blank? || refresh_token.blank?
|
67
|
+
|
68
|
+
return true if expires_in.blank?
|
69
|
+
|
70
|
+
token_retrieval_time.to_i + expires_in - DateTime.now.to_i < 60
|
71
|
+
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
def able_to_refresh?
|
75
|
+
refresh_token.present? && token_url.present?
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
def confidential_client?
|
80
|
+
client_id.present? && client_secret.present?
|
81
|
+
end
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
def oauth2_refresh_params
|
85
|
+
{
|
86
|
+
'grant_type' => 'refresh_token',
|
87
|
+
'refresh_token' => refresh_token
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# @api private
|
92
|
+
def oauth2_refresh_headers
|
93
|
+
base_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
94
|
+
|
95
|
+
return base_headers unless confidential_client?
|
96
|
+
|
97
|
+
credentials = "#{client_id}:#{client_secret}"
|
98
|
+
|
99
|
+
base_headers.merge(
|
100
|
+
'Authorization' => "Basic #{Base64.strict_encode64(credentials)}"
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
def update_from_response_body(request)
|
105
|
+
token_response_body = JSON.parse(request.response_body)
|
106
|
+
|
107
|
+
expires_in = token_response_body['expires_in'].is_a?(Numeric) ? token_response_body['expires_in'] : nil
|
108
|
+
|
109
|
+
self.access_token = token_response_body['access_token']
|
110
|
+
self.refresh_token = token_response_body['refresh_token']
|
111
|
+
self.expires_in = expires_in
|
112
|
+
self.token_retrieval_time = DateTime.now
|
113
|
+
|
114
|
+
add_to_client(client)
|
115
|
+
self
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/inferno/dsl/runnable.rb
CHANGED
@@ -229,12 +229,14 @@ module Inferno
|
|
229
229
|
# @param input_definition [Hash] options for input such as type, description, or title
|
230
230
|
# @option input_definition [String] :title Human readable title for input
|
231
231
|
# @option input_definition [String] :description Description for the input
|
232
|
-
# @option input_definition [String] :type text | textarea
|
232
|
+
# @option input_definition [String] :type text | textarea | radio
|
233
233
|
# @option input_definition [String] :default The default value for the input
|
234
234
|
# @option input_definition [Boolean] :optional Set to true to not require input for test execution
|
235
|
+
# @option input_definition [Hash] :options Possible input option formats based on input type
|
236
|
+
# @option options [Array] :list_options Array of options for input formats that require a list of possible values
|
235
237
|
# @return [void]
|
236
238
|
# @example
|
237
|
-
# input :
|
239
|
+
# input :patient_id, title: 'Patient ID', description: 'The ID of the patient being searched for',
|
238
240
|
# default: 'default_patient_id'
|
239
241
|
# @example
|
240
242
|
# input :textarea, title: 'Textarea Input Example', type: 'textarea', optional: true
|
@@ -252,14 +254,24 @@ module Inferno
|
|
252
254
|
|
253
255
|
# Define outputs
|
254
256
|
#
|
255
|
-
# @param
|
257
|
+
# @param identifier [Symbol] identifier for the output
|
258
|
+
# @param other_identifiers [Symbol] array of symbols if specifying multiple outputs
|
259
|
+
# @param output_definition [Hash] options for output
|
260
|
+
# @option output_definition [String] :type text | textarea | oauth_credentials
|
256
261
|
# @return [void]
|
257
262
|
# @example
|
258
|
-
# output :patient_id, :
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
+
# output :patient_id, :condition_id, :observation_id
|
264
|
+
# @example
|
265
|
+
# output :oauth_credentials, type: 'oauth_credentials'
|
266
|
+
def output(identifier, *other_identifiers, **output_definition)
|
267
|
+
if other_identifiers.present?
|
268
|
+
[identifier, *other_identifiers].compact.each do |output_identifier|
|
269
|
+
outputs << output_identifier
|
270
|
+
config.add_output(output_identifier)
|
271
|
+
end
|
272
|
+
else
|
273
|
+
outputs << identifier
|
274
|
+
config.add_output(identifier, output_definition)
|
263
275
|
end
|
264
276
|
end
|
265
277
|
|
@@ -186,6 +186,12 @@ module Inferno
|
|
186
186
|
.map { |header_name, value| Header.new(name: header_name.downcase, value: value, type: 'request') }
|
187
187
|
response_headers = response[:headers]
|
188
188
|
.map { |header_name, value| Header.new(name: header_name.downcase, value: value, type: 'response') }
|
189
|
+
request_body =
|
190
|
+
if request.dig(:headers, 'Content-Type')&.include?('application/x-www-form-urlencoded')
|
191
|
+
URI.encode_www_form(request[:payload])
|
192
|
+
else
|
193
|
+
request[:payload]
|
194
|
+
end
|
189
195
|
|
190
196
|
new(
|
191
197
|
verb: request[:method],
|
@@ -193,7 +199,7 @@ module Inferno
|
|
193
199
|
direction: direction,
|
194
200
|
name: name,
|
195
201
|
status: response[:code].to_i,
|
196
|
-
request_body:
|
202
|
+
request_body: request_body,
|
197
203
|
response_body: response[:body],
|
198
204
|
test_session_id: test_session_id,
|
199
205
|
headers: request_headers + response_headers
|
data/lib/inferno/exceptions.rb
CHANGED
@@ -62,5 +62,24 @@ module Inferno
|
|
62
62
|
super('The chosen runnable must be run as part of a group')
|
63
63
|
end
|
64
64
|
end
|
65
|
+
|
66
|
+
class UnknownAttributeException < RuntimeError
|
67
|
+
def initialize(attributes, klass)
|
68
|
+
attributes_string = attributes.map { |attribute| "'#{attribute}'" }.join(', ')
|
69
|
+
super("Unknown attributes for #{klass.name}: #{attributes_string}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class UnknownSessionDataType < RuntimeError
|
74
|
+
def initialize(output)
|
75
|
+
super("Unknown type '#{output[:type]}' for '#{output[:name]}'.")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class BadSessionDataType < RuntimeError
|
80
|
+
def initialize(name, expected_class, actual_class)
|
81
|
+
super("Expected '#{name}' to be a #{expected_class.name}, but found a #{actual_class.name}.")
|
82
|
+
end
|
83
|
+
end
|
65
84
|
end
|
66
85
|
end
|
@@ -1 +1 @@
|
|
1
|
-
(self.webpackChunkinferno_web_app=self.webpackChunkinferno_web_app||[]).push([[217],{3217:(t,e,n)=>{
|
1
|
+
"use strict";(self.webpackChunkinferno_web_app=self.webpackChunkinferno_web_app||[]).push([[217],{3217:(t,e,n)=>{n.r(e),n.d(e,{getCLS:()=>v,getFCP:()=>g,getFID:()=>h,getLCP:()=>y,getTTFB:()=>F});var i,a,r=function(){return"".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)},o=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1;return{name:t,value:e,delta:0,entries:[],id:r(),isFinal:!1}},s=function(t,e){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){var n=new PerformanceObserver((function(t){return t.getEntries().map(e)}));return n.observe({type:t,buffered:!0}),n}}catch(t){}},u=!1,c=!1,p=function(t){u=!t.persisted},f=function(){addEventListener("pagehide",p),addEventListener("beforeunload",(function(){}))},l=function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];c||(f(),c=!0),addEventListener("visibilitychange",(function(e){var n=e.timeStamp;"hidden"===document.visibilityState&&t({timeStamp:n,isUnloading:u})}),{capture:!0,once:e})},d=function(t,e,n,i){var a;return function(){n&&e.isFinal&&n.disconnect(),e.value>=0&&(i||e.isFinal||"hidden"===document.visibilityState)&&(e.delta=e.value-(a||0),(e.delta||e.isFinal||void 0===a)&&(t(e),a=e.value))}},v=function(t){var e,n=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=o("CLS",0),a=function(t){t.hadRecentInput||(i.value+=t.value,i.entries.push(t),e())},r=s("layout-shift",a);r&&(e=d(t,i,r,n),l((function(t){var n=t.isUnloading;r.takeRecords().map(a),n&&(i.isFinal=!0),e()})))},m=function(){return void 0===i&&(i="hidden"===document.visibilityState?0:1/0,l((function(t){var e=t.timeStamp;return i=e}),!0)),{get timeStamp(){return i}}},g=function(t){var e,n=o("FCP"),i=m(),a=s("paint",(function(t){"first-contentful-paint"===t.name&&t.startTime<i.timeStamp&&(n.value=t.startTime,n.isFinal=!0,n.entries.push(t),e())}));a&&(e=d(t,n,a))},h=function(t){var e=o("FID"),n=m(),i=function(t){t.startTime<n.timeStamp&&(e.value=t.processingStart-t.startTime,e.entries.push(t),e.isFinal=!0,r())},a=s("first-input",i),r=d(t,e,a);a?l((function(){a.takeRecords().map(i),a.disconnect()}),!0):window.perfMetrics&&window.perfMetrics.onFirstInputDelay&&window.perfMetrics.onFirstInputDelay((function(t,i){i.timeStamp<n.timeStamp&&(e.value=t,e.isFinal=!0,e.entries=[{entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+t}],r())}))},S=function(){return a||(a=new Promise((function(t){return["scroll","keydown","pointerdown"].map((function(e){addEventListener(e,t,{once:!0,passive:!0,capture:!0})}))}))),a},y=function(t){var e,n=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=o("LCP"),a=m(),r=function(t){var n=t.startTime;n<a.timeStamp?(i.value=n,i.entries.push(t)):i.isFinal=!0,e()},u=s("largest-contentful-paint",r);if(u){e=d(t,i,u,n);var c=function(){i.isFinal||(u.takeRecords().map(r),i.isFinal=!0,e())};S().then(c),l(c,!0)}},F=function(t){var e,n=o("TTFB");e=function(){try{var e=performance.getEntriesByType("navigation")[0]||function(){var t=performance.timing,e={entryType:"navigation",startTime:0};for(var n in t)"navigationStart"!==n&&"toJSON"!==n&&(e[n]=Math.max(t[n]-t.navigationStart,0));return e}();n.value=n.delta=e.responseStart,n.entries=[e],n.isFinal=!0,t(n)}catch(t){}},"complete"===document.readyState?setTimeout(e,0):addEventListener("pageshow",e)}}}]);
|