Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add extra options for modifying upload rates to the webpack plugin #6

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
52 changes: 0 additions & 52 deletions webpack/README.md

This file was deleted.

85 changes: 85 additions & 0 deletions webpack/raygun-webpack-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Raygun Webpack Plugin
The Raygun Webpack plugin automatically detects sourcemaps generated during your builds and sends them to Raygun, facilitating better error diagnostics by mapping minified code back to your original source code.

## Installation

Npm:
```
npm install @raygun/webpack-plugin
```

Yarn:
```
yarn add @raygun/webpack-plugin
```

Bun:
```
bun install @raygun/webpack-plugin
```

## Usage
```js
const RaygunWebpackPlugin = require('raygun-webpack-plugin');

module.exports = {
// Your existing webpack config...
plugins: [
new RaygunWebpackPlugin({
// Required options
baseUri: 'YOUR_WEBSITE_BASE_URI',
applicationId: 'YOUR_APPLICATION_ID',
patToken: 'YOUR_PAT_TOKEN',
})
]
};
```

## Configuration Options
Required Options:
- `baseUri`: Specifies the base URI for your website E.g. `http://localhost:3000/`.
- `applicationId`: Your Raygun application identifier.
- `patToken`: Your Raygun Personal Access Token with Sourcemap write permissions. Can be generated here: https://app.raygun.com/user/tokens

Rate Limiting Help:
If you're encountering rate limits when uploading sourcemaps, you can add these optional configurations:

```js
new RaygunWebpackPlugin({
// Required options...

// Optional: Add delay between file uploads
delayBetweenFiles: 2000, // Wait 2 seconds between files

// Optional: Customize retry behavior
retryConfig: {
maxRetries: 5, // Try up to 5 times (default: 3)
initialDelay: 2000, // Start with 2 second delay (default: 1000)
maxDelay: 60000, // Cap delays at 1 minute (default: 30000)
backoffFactor: 3 // Triple the delay after each attempt (default: 2)
}
})
```

The plugin includes built-in retry logic for failed uploads:
- Failed uploads will be retried up to 3 times with exponential backoff
- Initial retry delay: 1 second
- Maximum retry delay: 30 seconds
- Backoff multiplier: 2

If you're experiencing rate limit issues, you can:
1. Add a delay between file uploads using `delayBetweenFiles`
2. Adjust the retry settings using `retryConfig`

## How it works
Raygun looks for sourcemaps based on the url for the .js file where the error occurred, when you upload a sourcemap you must also provide that url as a key for the map file. The plugin - using your base URI - will find all built sourcemaps and will attempt to construct the URL based on the build directory.

# Development

## Building
```
npm run build
```

# Support
For support with the Raygun Webpack Plugin, please open an issue in our GitHub repository or reach out via our [support form](https://raygun.com/about/contact).
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,38 @@ import * as https from 'https';
import { Compiler } from 'webpack';

interface RaygunWebpackPluginOptions {
// Required options
applicationId: string;
patToken: string;
baseUri: string;
// Optional rate limiting configuration
retryConfig?: {
maxRetries?: number; // Maximum number of retry attempts (default: 3)
initialDelay?: number; // Initial delay in ms before first retry (default: 1000)
maxDelay?: number; // Maximum delay in ms between retries (default: 30000)
backoffFactor?: number; // Multiplier for exponential backoff (default: 2)
};
delayBetweenFiles?: number; // Add delay between file uploads if you're hitting rate limits
}

export class RaygunWebpackPlugin {
private options: RaygunWebpackPluginOptions;
private readonly defaultRetryConfig = {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 30000,
backoffFactor: 2
};

constructor(options: RaygunWebpackPluginOptions) {
this.options = options;
// Always have retry config, using defaults if not provided
this.options = {
...options,
retryConfig: {
...this.defaultRetryConfig,
...options.retryConfig
}
};
}

public apply(compiler: Compiler): void {
Expand All @@ -20,16 +42,23 @@ export class RaygunWebpackPlugin {
if (filepath.endsWith('.map')) {
const sourceMapContent = compilation.assets[filepath].source();
if (!!sourceMapContent) {

const uploadOperation = async () => {
await this.uploadSourceMap(filepath, typeof sourceMapContent === 'string' ? sourceMapContent : sourceMapContent.toString());
await this.uploadSourceMap(
filepath,
typeof sourceMapContent === 'string' ? sourceMapContent : sourceMapContent.toString()
);
console.log(`Successfully uploaded ${filepath} to Raygun.`);
};

try {
await this.retryOperation(uploadOperation, 3, 10);
await this.retryOperation(uploadOperation);

// Only add delay if explicitly configured
if (this.options.delayBetweenFiles) {
await this.delay(this.options.delayBetweenFiles);
}
} catch (error) {
console.error(`Error uploading ${filepath}: ${error}`);
console.error(`Failed to upload ${filepath} after ${this.options.retryConfig!.maxRetries} retries: ${error}`);
}
} else {
console.error(`No source map content found for ${filepath}`);
Expand All @@ -41,7 +70,6 @@ export class RaygunWebpackPlugin {
});
}


private uploadSourceMap(filePath: string, sourceMapContent: string): Promise<void> {
return new Promise((resolve, reject) => {
const { baseUri, applicationId, patToken } = this.options;
Expand Down Expand Up @@ -76,19 +104,15 @@ export class RaygunWebpackPlugin {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
// Ensure statusCode is defined and check the range
if (typeof res.statusCode === 'number' && res.statusCode >= 200 && res.statusCode < 300) {
resolve();
} else {
const errorMessage = `Failed to upload source map: HTTP status code ${res.statusCode || 'undefined'}`;
console.error(errorMessage);
reject(new Error(errorMessage));
reject(new Error(`HTTP status code ${res.statusCode || 'undefined'}`));
}
});
});

req.on('error', (error) => {
console.error(`Request error: ${error}`);
reject(error);
});

Expand All @@ -97,32 +121,33 @@ export class RaygunWebpackPlugin {
});
}

private getFileNameFromPath(filePath: string) {
// Normalize the path to use a consistent separator
private getFileNameFromPath(filePath: string): string {
const normalizedPath = filePath.replace(/\\/g, '/');
// Split the path by the directory separator and get the last element
const parts = normalizedPath.split('/');
return parts.pop(); // Extracts and returns the last segment (the file name)
return parts.pop() || '';
}

private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Function to attempt an operation with retries and exponential backoff
private async retryOperation<T>(operation: () => Promise<T>, retries: number, delayLength: number): Promise<boolean> {
try {
await operation();
return true;
} catch (error) {
if (retries > 0) {
console.error(`Attempt failed, retrying... (${retries} retries left)`);
await this.delay(delayLength);
return this.retryOperation(operation, retries - 1, delayLength * 2);
} else {
console.error(`All retry attempts failed: ${error}`);
return false;
private async retryOperation<T>(operation: () => Promise<T>): Promise<T> {
const config = this.options.retryConfig!;
let retries = 0;
let delay = config.initialDelay!;

while (true) {
try {
return await operation();
} catch (error) {
retries++;
if (retries > config.maxRetries!) {
throw error;
}

await this.delay(delay);
delay = Math.min(delay * config.backoffFactor!, config.maxDelay!);
}
}
}
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 4 additions & 0 deletions webpack/raygun-webpack-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
src/*.js
src/*.css
dist/
node_modules/
58 changes: 58 additions & 0 deletions webpack/raygun-webpack-test/generate-entries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// generate-entries.js
const fs = require('fs');
const path = require('path');

const sourceDir = path.join(__dirname, 'src');

// Create src directory if it doesn't exist
if (!fs.existsSync(sourceDir)) {
fs.mkdirSync(sourceDir);
}

// Generate 150 JS files
for (let i = 1; i <= 150; i++) {
const jsContent = `// entry${i}.js

// Each file has unique content and calculations
const uniqueValue${i} = ${Math.random() * 1000};

export class Example${i} {
constructor() {
this.value = 'Example ${i}';
this.uniqueId = uniqueValue${i};
}

calculate() {
return Math.sin(this.uniqueId) * ${i};
}

doSomething() {
console.log(this.value, this.calculate());
}
}

const example = new Example${i}();
example.doSomething();
`;

const cssContent = `
/* styles${i}.css */
.example-${i} {
color: #${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')};
padding: ${i % 20 + 10}px;
margin: ${i % 15 + 5}px;
border: ${i % 5 + 1}px solid #ccc;
border-radius: ${i % 10 + 2}px;
}

.unique-${i} {
background: linear-gradient(${i % 360}deg, #${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}, #${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')});
transform: rotate(${i}deg);
}
`;

fs.writeFileSync(path.join(sourceDir, `entry${i}.js`), jsContent);
fs.writeFileSync(path.join(sourceDir, `styles${i}.css`), cssContent);
}

console.log('Generated 150 entry points with CSS files');
29 changes: 29 additions & 0 deletions webpack/raygun-webpack-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "raygun-webpack-test",
"version": "1.0.0",
"description": "Test project for Raygun Webpack Plugin",
"main": "index.js",
"scripts": {
"generate": "bun generate-entries.js",
"build": "webpack --config webpack.config.js",
"test": "bun run generate && bun run build"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.23.7",
"@babel/preset-env": "^7.23.7",
"babel-loader": "^9.1.3",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"mini-css-extract-plugin": "^2.7.6",
"terser-webpack-plugin": "^5.3.10",
"@raygun.io/webpack-plugin": "link:@raygun.io/webpack-plugin",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"path-browserify": "^1.0.1"
}
}
2 changes: 2 additions & 0 deletions webpack/raygun-webpack-test/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
1. Add your Raygun credentials to `webpack.config.js` and run `npm install` to install the dependencies.
2. Run `npm run generate` to generate the entry points and `npm run build` to build the project.
Loading