Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add --idle-timeout flag #74

Merged
merged 3 commits into from
Aug 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions features/browser-upload-multiple.feature
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,26 @@ Feature: Browser source map upload multiple
And the sourcemap payload field "sourceMap" matches the source map "main-b3944033.json" for "multiple-source-map-webpack"
And the sourcemap payload field "minifiedFile" matches the minified file "main-b3944033.js" for "multiple-source-map-webpack"
And the Content-Type header is valid multipart form-data

Scenario: A request can set a timeout using the --idle-timeout flag
When I set the response delay to 5000 milliseconds
And I run the service "multiple-source-map-webpack" with the command
"""
bugsnag-source-maps upload-browser
--api-key 123
--app-version 2.0.0
--directory dist
--base-url "http://myapp.url/static/js/"
--endpoint http://maze-runner:9339
--idle-timeout 0.0008 # approx 50ms in minutes
"""
Then the last run docker command did not exit successfully
And the last run docker command output "The request timed out."
When I wait to receive 5 sourcemaps
Then the Content-Type header is valid multipart form-data for all requests
And the sourcemap payload field "apiKey" equals "123" for all requests
And the sourcemap payload field "appVersion" equals "2.0.0" for all requests
And the sourcemap payload field "overwrite" is null for all requests
And the sourcemap payload field "minifiedUrl" equals "http://myapp.url/static/js/main-b3944033.js" for all requests
And the sourcemap payload field "sourceMap" matches the source map "main-b3944033.json" for "multiple-source-map-webpack" for all requests
And the sourcemap payload field "minifiedFile" matches the minified file "main-b3944033.js" for "multiple-source-map-webpack" for all requests
26 changes: 25 additions & 1 deletion features/browser-upload-one.feature
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Feature: Browser source map upload one
--bundle-url "http://myapp.url/static/js/main.js"
--endpoint http://maze-runner:9339
"""
And I wait to receive 5 sourcemap
And I wait to receive 5 sourcemaps
Then the last run docker command did not exit successfully
And the last run docker command output "A server side error occurred while processing the upload."
And the last run docker command output "HTTP status 500 received from upload API"
Expand Down Expand Up @@ -271,3 +271,27 @@ Feature: Browser source map upload one
And the sourcemap payload field "sourceMap" matches the expected source map for "single-source-map-webpack"
And the sourcemap payload field "minifiedFile" matches the expected minified file for "single-source-map-webpack"
And the Content-Type header is valid multipart form-data

Scenario: A request can set a timeout using the --idle-timeout flag
When I set the response delay to 5000 milliseconds
And I run the service "single-source-map-webpack" with the command
"""
bugsnag-source-maps upload-browser
--api-key 123
--app-version 2.0.0
--source-map dist/main.js.map
--bundle dist/main.js
--bundle-url "http://myapp.url/static/js/main.js"
--endpoint http://maze-runner:9339
--idle-timeout 0.0008 # approx 50ms in minutes
"""
Then the last run docker command did not exit successfully
And the last run docker command output "The request timed out."
When I wait to receive 5 sourcemaps
And the Content-Type header is valid multipart form-data for all requests
And the sourcemap payload field "apiKey" equals "123" for all requests
And the sourcemap payload field "appVersion" equals "2.0.0" for all requests
And the sourcemap payload field "overwrite" is null for all requests
And the sourcemap payload field "minifiedUrl" equals "http://myapp.url/static/js/main.js" for all requests
And the sourcemap payload field "sourceMap" matches the expected source map for "single-source-map-webpack" for all requests
And the sourcemap payload field "minifiedFile" matches the expected minified file for "single-source-map-webpack" for all requests
22 changes: 22 additions & 0 deletions features/node-upload-multiple.feature
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,25 @@ Feature: Node source map upload multiple
And the sourcemap payload field "sourceMap" matches the source map "main-b3944033.json" for "multiple-source-map-webpack"
And the sourcemap payload field "minifiedFile" matches the minified file "main-b3944033.js" for "multiple-source-map-webpack"
And the Content-Type header is valid multipart form-data

Scenario: A request can set a timeout using the --idle-timeout flag
When I set the response delay to 5000 milliseconds
And I run the service "multiple-source-map-webpack" with the command
"""
bugsnag-source-maps upload-node
--api-key 123
--app-version 2.0.0
--directory dist
--endpoint http://maze-runner:9339
--idle-timeout 0.0008 # approx 50ms in minutes
"""
Then the last run docker command did not exit successfully
And the last run docker command output "The request timed out."
When I wait to receive 5 sourcemaps
And the Content-Type header is valid multipart form-data for all requests
And the sourcemap payload field "apiKey" equals "123" for all requests
And the sourcemap payload field "appVersion" equals "2.0.0" for all requests
And the sourcemap payload field "overwrite" is null for all requests
And the sourcemap payload field "minifiedUrl" equals "dist/main-b3944033.js" for all requests
And the sourcemap payload field "sourceMap" matches the source map "main-b3944033.json" for "multiple-source-map-webpack" for all requests
And the sourcemap payload field "minifiedFile" matches the minified file "main-b3944033.js" for "multiple-source-map-webpack" for all requests
23 changes: 23 additions & 0 deletions features/node-upload-one.feature
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,26 @@ Feature: Node source map upload one
And the sourcemap payload field "sourceMap" matches the expected source map for "single-source-map-webpack"
And the sourcemap payload field "minifiedFile" matches the expected minified file for "single-source-map-webpack"
And the Content-Type header is valid multipart form-data

Scenario: A request can set a timeout using the --idle-timeout flag
When I set the response delay to 5000 milliseconds
And I run the service "single-source-map-webpack" with the command
"""
bugsnag-source-maps upload-node
--api-key 123
--app-version 2.0.0
--source-map dist/main.js.map
--bundle dist/main.js
--endpoint http://maze-runner:9339
--idle-timeout 0.0008 # approx 50ms in minutes
"""
Then the last run docker command did not exit successfully
And the last run docker command output "The request timed out."
When I wait to receive 5 sourcemaps
And the Content-Type header is valid multipart form-data for all requests
And the sourcemap payload field "apiKey" equals "123" for all requests
And the sourcemap payload field "appVersion" equals "2.0.0" for all requests
And the sourcemap payload field "overwrite" is null for all requests
And the sourcemap payload field "minifiedUrl" equals "dist/main.js" for all requests
And the sourcemap payload field "sourceMap" matches the expected source map for "single-source-map-webpack" for all requests
And the sourcemap payload field "minifiedFile" matches the expected minified file for "single-source-map-webpack" for all requests
21 changes: 21 additions & 0 deletions features/react-native-fetch.feature
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,24 @@ Feature: React native source map fetch mode
And the sourcemap payload field "sourceMap" matches the expected source map for "fetch-react-native-0-60-ios"
And the sourcemap payload field "bundle" matches the expected bundle for "fetch-react-native-0-60-ios"
And the Content-Type header is valid multipart form-data

Scenario: A request can set a timeout using the --idle-timeout flag
When I start the service "react-native-0-60-bundler"
And I wait for 2 seconds
And I set the response delay to 5000 milliseconds
And I run the service "react-native-0-60-fetch" with the command
"""
bugsnag-source-maps upload-react-native
--api-key 123
--app-version 2.0.0
--endpoint http://maze-runner:9339
--fetch
--bundler-url http://react-native-0-60-bundler:9449
--platform ios
--idle-timeout 0.0008 # approx 50ms in minutes
"""
Then the last run docker command did not exit successfully
And the last run docker command output "The request to http://react-native-0-60-bundler:9449 timed out."
# this is fetch mode so no sourcemaps should be uploaded because the request
# to the RN bundle server should timeout due to the idle-timeout
And I should receive no requests
23 changes: 23 additions & 0 deletions features/react-native-upload-one.feature
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,26 @@ Feature: React native source map upload one
And the sourcemap payload field "sourceMap" matches the expected source map for "single-source-map-react-native-0-60-ios"
And the sourcemap payload field "bundle" matches the expected bundle for "single-source-map-react-native-0-60-ios"
And the Content-Type header is valid multipart form-data

Scenario: A request will be retried up to 5 times on a server failure (500 status code)
When I set the response delay to 5000 milliseconds
And I run the service "single-source-map-react-native-0-60-ios" with the command
"""
bugsnag-source-maps upload-react-native
--api-key 123
--app-version 2.0.0
--endpoint http://maze-runner:9339
--source-map build/source-map.json
--bundle build/bundle.js
--platform ios
--idle-timeout 0.0008 # approx 50ms in minutes
"""
Then the last run docker command did not exit successfully
And the last run docker command output "The request timed out."
When I wait to receive 5 sourcemaps
And the Content-Type header is valid multipart form-data for all requests
And the sourcemap payload field "apiKey" equals "123" for all requests
And the sourcemap payload field "appVersion" equals "2.0.0" for all requests
And the sourcemap payload field "overwrite" equals "true" for all requests
And the sourcemap payload field "sourceMap" matches the expected source map for "single-source-map-react-native-0-60-ios" for all requests
And the sourcemap payload field "bundle" matches the expected bundle for "single-source-map-react-native-0-60-ios" for all requests
44 changes: 34 additions & 10 deletions src/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,25 @@ interface ReactNativePayload {
bundle: File
}

interface RequestOptions {
idleTimeout?: number
}

const MAX_ATTEMPTS = 5
const RETRY_INTERVAL_MS = parseInt(process.env.BUGSNAG_RETRY_INTERVAL_MS as string) || 1000
const TIMEOUT_MS = parseInt(process.env.BUGSNAG_TIMEOUT_MS as string) || 30000

export default async function request (endpoint: string, payload: Payload, requestOpts: http.RequestOptions): Promise<void> {
const DEFAULT_TIMEOUT_MS = parseInt(process.env.BUGSNAG_TIMEOUT_MS as string) || 60000

export default async function request (
endpoint: string,
payload: Payload,
requestOpts: http.RequestOptions,
options: RequestOptions = {}
): Promise<void> {
let attempts = 0
const go = async (): Promise<void> => {
try {
attempts++
await send(endpoint, payload, requestOpts)
await send(endpoint, payload, requestOpts, options)
} catch (err) {
if (err && err.isRetryable !== false && attempts < MAX_ATTEMPTS) {
await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL_MS))
Expand Down Expand Up @@ -109,7 +118,12 @@ function appendReactNativeFormData(formData: FormData, payload: ReactNativePaylo
return formData
}

export async function send (endpoint: string, payload: Payload, requestOpts: http.RequestOptions): Promise<void> {
export async function send (
endpoint: string,
payload: Payload,
requestOpts: http.RequestOptions,
options: RequestOptions = {}
): Promise<void> {
return new Promise<void>((resolve, reject) => {
const formData = createFormData(payload)

Expand Down Expand Up @@ -153,7 +167,7 @@ export async function send (endpoint: string, payload: Payload, requestOpts: htt
formData.pipe(req)

addErrorHandler(req, reject)
addTimeout(req, reject)
addTimeout(req, reject, options)
})
}

Expand All @@ -169,7 +183,7 @@ export function isRetryable (status?: number): boolean {
)
}

export function fetch(endpoint: string): Promise<string> {
export function fetch(endpoint: string, options: RequestOptions = {}): Promise<string> {
return new Promise<string>((resolve, reject) => {
const parsedUrl = url.parse(endpoint)

Expand Down Expand Up @@ -197,7 +211,7 @@ export function fetch(endpoint: string): Promise<string> {
})

addErrorHandler(req, reject)
addTimeout(req, reject)
addTimeout(req, reject, options)
})
}

Expand All @@ -218,8 +232,18 @@ function addErrorHandler(req: http.ClientRequest, reject: (reason: NetworkError)
})
}

function addTimeout(req: http.ClientRequest, reject: (reason: NetworkError) => void): void {
req.setTimeout(TIMEOUT_MS, () => {
const minutesToMilliseconds = (minutes: number): number => minutes * 60 * 1000

function addTimeout(
req: http.ClientRequest,
reject: (reason: NetworkError) => void,
options: RequestOptions
): void {
const timeout = options.idleTimeout
? minutesToMilliseconds(options.idleTimeout)
: DEFAULT_TIMEOUT_MS

req.setTimeout(timeout, () => {
const err = new NetworkError('Connection timed out')
err.code = NetworkErrorCode.TIMEOUT
reject(err)
Expand Down
3 changes: 2 additions & 1 deletion src/commands/CommandDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export const commonCommandDefs = [
{ name: 'project-root', type: String, description: 'the top level directory of your project (defaults to the current directory)' },
{ name: 'endpoint', type: String, description: 'customize the endpoint for Bugsnag On-Premise' },
{ name: 'quiet', type: Boolean, description: 'less verbose logging' },
{ name: 'code-bundle-id', type: String }
{ name: 'code-bundle-id', type: String },
{ name: 'idle-timeout', type: Number, description: 'idle timeout for HTTP requests in minutes' }
]
2 changes: 2 additions & 0 deletions src/commands/UploadBrowserCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default async function uploadBrowser (argv: string[], opts: Record<string
endpoint: browserOpts.endpoint,
detectAppVersion: browserOpts.detectAppVersion,
codeBundleId: browserOpts.codeBundleId,
idleTimeout: browserOpts.idleTimeout,
logger
})
} else {
Expand All @@ -64,6 +65,7 @@ export default async function uploadBrowser (argv: string[], opts: Record<string
endpoint: browserOpts.endpoint,
detectAppVersion: browserOpts.detectAppVersion,
codeBundleId: browserOpts.codeBundleId,
idleTimeout: browserOpts.idleTimeout,
logger
})
}
Expand Down
2 changes: 2 additions & 0 deletions src/commands/UploadNodeCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default async function uploadNode (argv: string[], opts: Record<string, u
endpoint: nodeOpts.endpoint,
detectAppVersion: nodeOpts.detectAppVersion,
codeBundleId: nodeOpts.codeBundleId,
idleTimeout: nodeOpts.idleTimeout,
logger
})
} else {
Expand All @@ -54,6 +55,7 @@ export default async function uploadNode (argv: string[], opts: Record<string, u
endpoint: nodeOpts.endpoint,
detectAppVersion: nodeOpts.detectAppVersion,
codeBundleId: nodeOpts.codeBundleId,
idleTimeout: nodeOpts.idleTimeout,
logger
})
}
Expand Down
2 changes: 2 additions & 0 deletions src/commands/UploadReactNativeCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default async function uploadReactNative (argv: string[], opts: Record<st
endpoint: reactNativeOpts.endpoint,
bundlerUrl: reactNativeOpts.bundlerUrl,
bundlerEntryPoint: reactNativeOpts.bundlerEntryPoint,
idleTimeout: reactNativeOpts.idleTimeout,
logger
})
} else {
Expand All @@ -71,6 +72,7 @@ export default async function uploadReactNative (argv: string[], opts: Record<st
platform: reactNativeOpts.platform,
dev: reactNativeOpts.dev,
endpoint: reactNativeOpts.endpoint,
idleTimeout: reactNativeOpts.idleTimeout,
logger
})
}
Expand Down
12 changes: 8 additions & 4 deletions src/uploaders/BrowserUploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ interface UploadSingleOpts {
overwrite?: boolean
projectRoot?: string
endpoint?: string
detectAppVersion?: boolean,
detectAppVersion?: boolean
idleTimeout?: number
requestOpts?: http.RequestOptions
logger?: Logger
}
Expand All @@ -52,6 +53,7 @@ export async function uploadOne ({
sourceMap,
appVersion,
codeBundleId,
idleTimeout,
overwrite = false,
projectRoot = process.cwd(),
endpoint = DEFAULT_UPLOAD_ORIGIN,
Expand Down Expand Up @@ -117,7 +119,7 @@ export async function uploadOne ({
minifiedFile: (bundleContent && fullBundlePath) ? new File(fullBundlePath, bundleContent) : undefined,
sourceMap: new File(fullSourceMapPath, JSON.stringify(transformedSourceMap)),
overwrite: overwrite
}, requestOpts)
}, requestOpts, { idleTimeout })

const uploadedFiles = (bundleContent && fullBundlePath) ? `${sourceMap} and ${bundle}` : sourceMap

Expand All @@ -141,7 +143,8 @@ interface UploadMultipleOpts {
overwrite?: boolean
projectRoot?: string
endpoint?: string
detectAppVersion?: boolean,
detectAppVersion?: boolean
idleTimeout?: number
requestOpts?: http.RequestOptions
logger?: Logger
}
Expand All @@ -161,6 +164,7 @@ export async function uploadMultiple ({
directory,
appVersion,
codeBundleId,
idleTimeout,
overwrite = false,
detectAppVersion = false,
projectRoot = process.cwd(),
Expand Down Expand Up @@ -249,7 +253,7 @@ export async function uploadMultiple ({
minifiedFile: (bundleContent && fullBundlePath) ? new File(fullBundlePath, bundleContent) : undefined,
sourceMap: new File(fullSourceMapPath, JSON.stringify(transformedSourceMap)),
overwrite: overwrite
}, requestOpts)
}, requestOpts, { idleTimeout })

const uploadedFiles = (bundleContent && fullBundlePath) ? `${sourceMap} and ${bundlePath}` : sourceMap

Expand Down
Loading