Giter Site home page Giter Site logo

mindscapehq / raygun4php Goto Github PK

View Code? Open in Web Editor NEW
36.0 29.0 38.0 559 KB

PHP provider for Raygun

Home Page: https://raygun.com

License: MIT License

PHP 99.05% Hack 0.95%
raygun php error-handler error-handling crash-reporting error-monitoring error-reporting composer crash-reports crash-reporting-tool

raygun4php's Introduction

Raygun4PHP

Raygun.com provider for PHP 7.2+

See v1.8 documentation for PHP v5.3+ support

Build Status

Installation

Raygun4PHP uses Guzzle to handle the transmission of data to the Raygun API. Having cURL installed is recommended, but is not required, as Guzzle will use a PHP stream wrapper if cURL is not installed.

With Composer

Composer is a package management tool for PHP which automatically fetches dependencies and also supports autoloading - this is a low-impact way to get Raygun4PHP into your site.

1. If you use a *nix environment, follow the instructions to install Composer. Windows users can run this installer to automatically add it to the Path.

2. Inside your project's root directory create a composer.json file, containing:

{
    "require": {
        "mindscape/raygun4php": "^2.0"
    }
}

3. From your shell run php composer.phar install (*nix) or composer install (Windows). This will download Raygun4PHP and create the appropriate autoload data.

4. Then in a PHP file just add:

require_once 'vendor/autoload.php';

and the library will be imported ready for use.

Usage

You can automatically send both PHP errors and object-oriented exceptions to Raygun. The RaygunClient requires an HTTP transport (e.g. Guzzle or other PSR-7 compatible interface).

There are Guzzle-based asynchronous and synchronous transport classes in the provider, or you can use your own.

Asynchronous transport

This is an example of basic usage for asynchronous sending of errors and exceptions:

<?php
namespace {
    require_once 'vendor/autoload.php';

    use GuzzleHttp\Client;
    use Raygun4php\RaygunClient;
    use Raygun4php\Transports\GuzzleAsync;

    $httpClient = new Client([
        'base_uri' => 'https://api.raygun.com',
        'timeout' => 2.0,
        'headers' => [
            'X-ApiKey' => 'INSERT_API_KEY_HERE'
        ]
    ]);

    $transport = new GuzzleAsync($httpClient);

    $raygunClient = new RaygunClient($transport);

    set_error_handler(function($errno, $errstr, $errfile, $errline) use ($raygunClient) {
        $raygunClient->SendError($errno, $errstr, $errfile, $errline);
    });

    set_exception_handler(function($exception) use ($raygunClient) {
        $raygunClient->SendException($exception);
    });

    register_shutdown_function(function() use ($raygunClient) {
        $lastError = error_get_last();

        if (!is_null($lastError)) {
            [$type, $message, $file, $line] = $lastError;
            $raygunClient->SendError($type, $message, $file, $line);
        }
    });

    register_shutdown_function([$transport, 'wait']);
}

Synchronous transport

For synchronous transport, use the snippet above but replace use Raygun4php\Transports\GuzzleAsync; with:

use Raygun4php\Transports\GuzzleSync;

And replace $transport = new GuzzleAsync($httpClient); with:

$transport = new GuzzleSync($httpClient);

Remove this line:

register_shutdown_function([$transport, 'wait']);

Note that if you are placing it inside a file with a namespace of your choosing, the above code should be declared to be within the global namespace (thus the namespace { } is required). You will also need whichever require statement as above (autoload or manual) before the $raygunClient instantiation.

Copy your application's API key from the Raygun app, and paste it into the X-ApiKey header field of the HTTP client.

If the handlers reside in their own file, just import it in every file where you'd like exceptions and errors to be sent, and they will be delivered to Raygun.

Configuration

Proxies

A URL can be set as the proxy property on the HTTP Client:

// ...

$httpClient = new Client([
    'base_uri' => 'https://api.raygun.com',
    'proxy' => 'http://someproxy:8080',
    'headers' => [
        'X-ApiKey' => 'INSERT_API_KEY_HERE'
    ]
]);

See Guzzle's proxy documentation for more options.

Debugging with a logger

We recommend using a logger which is compatible with the PSR-3 LoggerInterface (e.g. Monolog).

Expanding on the Asynchronous transport example above, you can set a logger to be used by the transport like so:

// ...

use Monolog\Handler\FirePHPHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;

// ...

$logger = new Logger('my_logger');
$logger->pushHandler(new StreamHandler(__DIR__ . '/debug.log'));
$logger->pushHandler(new FirePHPHandler());

// Create $httpClient ...

$transport = new GuzzleAsync($httpClient);
$transport->setLogger($logger);

$raygunClient = new RaygunClient($transport);

// Set error handlers ...

Response codes

  • 202: Message received by Raygun API correctly
  • 400: Bad Request. This may indicate an invalid payload - please contact Raygun if you continue to see this.
  • 403: Invalid API key. Copy the API key from the Raygun app setup instructions or application settings

Version numbers

You can transmit the version number of your PHP project along with the message by calling SetVersion() on your RaygunClient after it is instantiated - this is optional but recommended as the version number is considered to be first-class data for a message.

$raygunClient = new RaygunClient($transport);
$raygunClient->SetVersion('1.0.0.0');

Adding Tags

Tags can be added to error data to provide extra information and to help filtering errors within Raygun. They are provided as an array of strings or numbers passed as the 5th argument to the SendError function and as the 2nd argument to the SendException function.

The declaration of the exception and error handlers for adding tags to each payload could look something like this:

<?php
// ...

$raygunClient = new RaygunClient($transport);

$tags = ['staging-environment', 'machine-4'];

set_error_handler(function($errno, $errstr, $errfile, $errline) use ($raygunClient, $tags) {
    $raygunClient->SendError($errno, $errstr, $errfile, $errline, $tags);
});

set_exception_handler(function($exception) use ($raygunClient, $tags) {
    $raygunClient->SendException($exception, $tags);
});

register_shutdown_function(function() use ($raygunClient, $tags) {
    $lastError = error_get_last();

    if (!is_null($lastError)) {
        [$type, $message, $file, $line] = $lastError;
        $raygunClient->SendError($type, $message, $file, $line, $tags);
    }
});

// ...

You can add tags when manually sending a handled exception like so:

try {
    // Do something questionable...
} catch(Exception $exception) {
    $raygunClient->SendException($exception, ['handled-exception']);
}

Affected user tracking

You can call $raygunClient->SetUser, passing in some or all of the following data, which will be used to provide an affected user count and reports:

$raygunClient->SetUser($user = 'email_or_unique_identifier', $firstName = 'Example', $fullName = 'Example User', $emailAddress = '[email protected]', $isAnonymous = false, $uuid = 'abc123');

$user should be a unique identifier which is used to identify your users. If you set this to their email address, be sure to also set the $email parameter too.

This feature and values are optional if you wish to disable it for privacy concerns. To do so, pass true in as the second parameter to the RaygunClient constructor.

// Disable user tracking:
$raygunClient = new RaygunClient($transport, true);

Note that this data is stored as cookies. If you do not call SetUser the default is to store a random UUID to represent the user.

This feature can be used in CLI mode by calling SetUser() at the start of your session.

Custom error grouping

Control of how error instances are grouped together can be achieved by passing a callback to the SetGroupingKey method on the client. If the callback returns a string, ideally 100 characters or less, errors matching that key will be grouped together, overriding the default automatic grouping. If the callback returns a non-string value then that error will be grouped automatically.

$raygunClient->SetGroupingKey(function($payload, $stackTrace) {
    // Inspect the above parameters and return a hash from the properties ...
    return $payload->Details->Error->Message; // Naive message-based grouping only
});

Filtering Sensitive Data

Some error data will be too sensitive to transmit to an external service, such as credit card details or passwords. Since this data is very application specific, Raygun doesn't filter out anything by default. You can configure to either replace or otherwise transform specific values based on their keys. These transformations apply to form data ($_POST), custom user data, HTTP headers, and environment data ($_SERVER). It does not filter the URL or its $_GET parameters, or custom message strings. Since Raygun doesn't log method arguments in stack traces, those don't need filtering. All key comparisons are case insensitive.

$raygunClient->setFilterParams([
    'password' => true,
    'creditcardnumber' => true,
    'ccv' => true,
    'php_auth_pw' => true, // filters basic auth from $_SERVER
]);
// Example input: ['Username' => 'myuser','Password' => 'secret']
// Example output: ['Username' => 'myuser','Password' => '[filtered]']

You can also define keys as regular expressions:

$raygunClient->setFilterParams([
    '/^credit/i' => true,
]);
// Example input: ['CreditCardNumber' => '4111111111111111','CreditCardCcv' => '123']
// Example output: ['CreditCardNumber' => '[filtered]','CreditCardCcv' => '[filtered]']

In case you want to retain some hints on the data rather than removing it completely, you can also apply custom transformations through PHP's anonymous functions. The following example truncates all keys starting with "address".

$raygunClient->setFilterParams([
    'Email' => function($key, $val) {return substr($val, 0, 5) . '...';}
]);
// Example input: ['Email' => '[email protected]']
// Example output: ['Email' => 'test@...']

If you want to ensure all form submission data is filtered out irrespective of field names for situations where there are a lot of forms that might request private information, you can do that too. The field names will still be transmitted, but the values will be filtered out.

$raygunClient->setFilterAllFormValues(true);

Note that when any filters are defined, the Raygun error will no longer contain the raw HTTP data, since there's no effective way to filter it.

Updating Cookie options

Cookies are used for the user tracking functionality of the Raygun4PHP provider. In version 1.8 of the provider, the options passed to the setcookie method can now be customized to your needs.

$raygunClient->SetCookieOptions([
    'expire'   => 2592000, // 30 * 24 * 60 * 60
    'path'     => '/',
    'domain'   => '',
    'secure'   => false,
    'httponly' => false
]);

Troubleshooting

As mentioned above, you can specify a logger to the HTTP transport:

$transport->setLogger($logger);

This will log out exceptions occurring while transmitting data to Raygun. Create an issue or contact us if you continue to have problems.

400 from command-line Posix environments

If, when running a PHP script from the command line on *nix operating systems, you receive a '400 Bad Request' error (in your debug log), check to see if you have any LESS_TERMCAP environment variables set. These are not compatible with the current version of Raygun4PHP. As a workaround, unset these variables before your script runs, then reset them afterwards.

Error Control Operators (@)

If you are using the setup as described above errors will be sent to Raygun regardless of any lines prepended with an error control operator (the @ symbol). To stop these errors from being sent to Raygun you can call PHP's error_reporting method which return 0 if the triggered error was preceded by an @ symbol.

Error handler example:

function ($errno, $errstr, $errfile, $errline) use ($raygunClient) {
    if (error_reporting() !== 0) {
        $raygunClient->SendError($errno, $errstr, $errfile, $errline);
    }
}

See the Error Control Operators section on PHP.net for more information.

Changelog

  • 2.3.2: Fix Don't save a null value with setcookie()
  • 2.3.1: Use iconv to convert from ISO-8859-1 instead of utf8_encode
  • 2.3.0: Support newer versions of psr/log
  • 2.2.1: SetUser method now supports numeric data types for 'user' parameter
  • 2.2.0: Capture the file and line number where the exception itself is thrown
  • 2.1.1: Fix namespace in Uuid library to prevent composer errors with mixed case package names
  • 2.1.0: Allow client IP address to be filtered out, add configuration to filter out all POSTed form data
  • 2.0.2: Remove PHP 7.2 and replace with PHP 8.0 in Travis builds, fix JSON match assertion issue in unit tests
  • 2.0.1: Fixes for CLI use, PHP 7.4 deprecation warning in RaygunMessage JSON encoding
  • 2.0.0: New major version
    • Increased minimum PHP version to 7.1
    • Added PSR-4 autoloader
    • Removes toJsonRemoveUnicodeSequences() and removeNullBytes() methods from the RaygunClient class - use toJson() instead
  • 1.8.4: PHPUnit and configuration updates, PHPDoc fixes, and a fixed null pointer exception accessing user ID from cookies
  • 1.8.3: Remove the --dev option for composer installations as it's now deprecated
  • 1.8.2: No longer output warning when a socket connection fails
  • 1.8.1: Fix issue with error being raised with null bytes send with escapeshellarg method
  • 1.8.0: Bugfix with multiple cookies being set. Cookie options can be set via the setCookieOptions method
  • 1.7.1: Fixed illegal string offset
  • 1.7.0: Added custom error grouping
  • 1.6.1: Assign ClassName as exceptionClass
  • 1.6.0: Added HTTP proxy support, support X-Forwarded-For, null server var guards
  • 1.5.3: Unify property casing (internal change)
  • 1.5.2: Prevent error when query_string isn't present in $_SERVER
  • 1.5.1: Guard against intermittent user id cookie being null; overload for disabling user tracking
  • 1.5.0: Add enhanced user data support; fix null backtrace frames that could occur in 1.4
  • 1.4.0: Added Sensitive Data Filtering; improved Error backtraces; Travis CI enabled
  • 1.3.6: Move included Rhumsaa\Uuid lib into this namespace to prevent collisions if already included
  • 1.3.5: Fixed possible bug in async curl logic
  • 1.3.4: Bugfix in request message for testing
  • 1.3.3: Hotfix for script error in v1.3.2
  • 1.3.2: UTF-8 encoding routine from previous version updated to remove PHP 5.5 deprecated function
  • 1.3.1: Request data, specifically $_SERVER variables, are now correctly encoded in UTF-8
  • 1.3: Added debug mode to output HTTP response code when in socket mode
  • 1.2.6: Fixed a bug in previous release rendering the UTC offset fix ineffective (thanks @mrardon for spotting this)
  • 1.2.5: Request rawData (php://input) limited to 4096 bytes in line with other providers; clamp UTC offset to sane values as API was seeing some entries with max int offsets
  • 1.2.4: Merged in unit tests
  • 1.2.3: Fixed a bug where OccurredOn wasn't in correct ISO 8601 representation
  • 1.2.2: Minor formatting refactor
  • 1.2.1: Several bugfixes for user tracking and request processing
  • 1.2: Added new async sending function; removed cURL dependency
  • 1.1: Added user tracking support; improved experience in CLI mode; add user-specified timestamp support; fixed user data encoding error
  • 1.0: Initial commit

raygun4php's People

Contributors

adamthehutt avatar anfly0 avatar benjaminharding avatar biggianteye avatar caseycs avatar chillu avatar cmdrkeen avatar darcythomas avatar davibennun avatar deacon-mcintyre avatar edsrzf avatar fundead avatar guysartorelli avatar hyungsul avatar ichaber avatar izsi-salmon avatar john-n-smith avatar localheinz avatar m3m0r7 avatar mrardon avatar noodlesnz avatar olwiba avatar redj4y avatar robbieaverill avatar szepeviktor avatar treehousetim avatar wildlyinaccurate avatar yakmoose avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

raygun4php's Issues

Fatal error w/ Rhumsaa\Uuid\Uuid

Hi,

My application uses composer to manage dependencies and autoloading. We're currently using the Rhumsaa\Uuid library in a number of places.

Raygun4Php uses the same library, but embeds it in the Raygun4Php code base. This means that we end up with a fatal error when the RaygunClient class is loaded, since on line 6 you've got:

require_once realpath(DIR . '/Uuid.php');

Since my application is already using the Rhumsaa\Uuid library, this results in the same namespace\class being redeclared.

If you want to embed the Rhumsaa\Uuid library inside Raygun4Php, then a better way to do this would be to include it in the Raygun4Php namespace as well. That way, an application that uses both would simply have duplicate code (i.e. Raygun4Php\Rhumsaa\Uuid and Rhumsaa\Uuid) without duplicate class names. This would prevent a fatal error and allow the libraries to peacefully coexist.

Alternatively, you could remove the require_once call and rely on autoloaders to handle the class loading.

Thanks,
Adam

Raygun doesn't actually report the file name and line number of the actual error, which is the most important part.

Identical to #72

I have to do something like this to even get the data to Raygun.

$raygunClient = new RaygunClient( $transport );
            $raygunClient->SendException(
                $exception,
                ['service' => env('APP_NAME' )],
                [
                    'file' => $exception->getFile(),
                    'line' => $exception->getLine()
                ]
            );

but then it's stuffed into the "Custom" tab and isn't part of the stack trace.

Something as simple as this change to BuildStackTrace in the RayGunExceptionMessage class would put file and line into the stack trace reported to RayGun.

    private function BuildStackTrace($exception)
    {
        $traces = $exception->getTrace();
        $lines = array();

        $line = new RaygunExceptionTraceLineMessage();
        $line->FileName = $exception->getFile();
        $line->LineNumber = $exception->getLine();
        $lines[] = $line;

        foreach ($traces as $trace) {
            $lines[] = $this->BuildLine($trace);
        }

        $this->StackTrace = $lines;
    }

Illegal string offset in PHP7.1

I'm getting the following error since Heroku upgraded its build pack to PHP 7.1.
Everything works fine after downgrading to 7.0.13

Uncaught ErrorException: Illegal string offset 'Total-Route-Time' in /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunRequestMessage.php:92

Stack trace: 
#0 /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunRequestMessage.php(92): Illuminate\Foundation\Bootstrap\HandleExceptions->handleError(2, 'Illegal string ...', '/app/vendor/min...', 92, Array) 
#1 /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunRequestMessage.php(35): Raygun4php\RaygunRequestMessage->emu_getAllHeaders() 
#2 /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunMessage.php(28): Raygun4php\RaygunRequestMessage->__construct() 
#3 /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php(234): Raygun4php\RaygunMessage->Build(Object(ErrorException)) 
#4 /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php(104): Raygun4php\RaygunClient->BuildMessage(Object(ErrorException), NULL) 
#5 /app/app/Core/Exceptions/Handler.php(59): Raygun4php\RaygunClient->SendException(Object(ErrorException)) 
#6 /app/bootstrap/cache/compil in /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunRequestMessage.php on line 92 

Parse error in src/Raygun4php/RaygunClient.php

There's a PHP parse error in src/Raygun4php/RaygunClient.php on line 295. There appears to be an extraneous parenthesis at the end of the line.

We're using this library in production via composer, so when changes are pushed that cause errors like this it's a big problem. If the master branch can't be reliably considered stable, then you should consider using tagged releases for packagist/composer.

Thanks,
Adam

Potential breaking change for rhumsaa/uuid

I'm getting the following error using Raygun4php:

Fatal error: Class 'Raygun4Php\Rhumsaa\Uuid\Uuid' not found in vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php on line 161

It looks like recently the author decided to re-namespace the package. From https://github.com/ramsey/uuid:

NOTICE: Formerly known as rhumsaa/uuid, The package and namespace names have changed to ramsey/uuid and Ramsey\Uuid, respectively.

It looks like this may be a breaking change for Raygun4Php since the library for generating UUIDs has been renamespaced out from under it.

However, upon investigating the source of raygun4php more, it looks like the Uuid file is bundled with the soruce as opposed to being loaded via composer, which is what I initially could lead to this error. If that's not the case, though, what could be leading to this error so far down in the Raygun4php source?

Thanks.

Installation instructions recommend dev-master version

The 'createapp' step over on Raygun.io recommends installing this library with:

{
  "require": {
    "mindscape/raygun4php": "dev-master"
  }
}

But including the library like that will break if this repo ever breaks BC (e.g. when a 2.0 version is prepared). It'd be better to recommend a version constraint like ~1.6.

Also, using dev-master leads to problems with minimum-stability. If you actually do want people running against a branch rather than a series of tags, the correct approach is to use aliases. That way, when master goes from being 1.x to 2.0-prerelease/alpha, those following the 1.x-dev branch will correctly stop following master.

.gitignore comment issue

.gitignore files are allowed to have comments however they must be at the beginning of the line not after pattern that is ignored.

The issue is here:

Generated_Code #added for RIA/Silverlight projects

We recently switched to using satis and this issue came up when trying to pull down the code for this. It consistently died with the following exception:

Dumping 'mindscape/raygun4php-1.2.1.0'.
  - Installing mindscape/raygun4php (1.2.1)
    Cloning e65e879e6e2ecc16208cbaab53a74154f6f11017




  [ErrorException]                    
  preg_match(): Unknown modifier 'a'  



Exception trace:
 () at /vendor/composer/composer/src/Composer/Package/Archiver/BaseExcludeFilter.php:62

Info on .gitignore is here: http://git-scm.com/book/en/Git-Basics-Recording-Changes-to-the-Repository#Ignoring-Files

The reason it is dying with that exception is due to how composer processes ignore files, namely it makes patterns to ignore and follows the standard set forward in the previous document. So for each line of .gitignore it makes a regex pattern using the delimiter of #

So if the pattern for the line referenced above is invalid because it is not ignored (required # at the beginning of the line):
#(?=[^\.])Generated_Code #added for RIA/(?=[^\.])Silverlight projects(?=$|/)#

JSON schema

Does anyone know if there is any official or reasonably correct JSON schema available for the /entries endpoint?

I'm currently looking at moving some methods from the RaygunClient class to the RaygunMessage class, and it would be great to have a schema to check the serialized message objects aginst.

The main problem I'm having is the documentation is not very clear on what fields are mandatory.

Provide a way to filter out IpAddress

Currently there is no way to filter out the IpAddress key from the request. Please provide a clean way to do this. We do not want to be sending clients' IP addresses nor our server IP address.

Treating ErrorException as a typical exception is not a good strategy

Using ErrorException to manage the data is fine, but then treating it as if it was a normal Exception is not.

Take the stack trace, for instance.
It does not make sense to persist the trace from the moment the ErrorException is created. This is redundant data. It also means that any info about line numbers is not persisted. $e->getLine() is not used anywhere despite it being the only place the error line is kept.

I can only deduce from this library that this is a service that was built only to handle exceptions correctly and hasn't been tested fully.

Along with a few other issues with the lib (including session monkeypatching), is there a plan to revisit this lib to make it a stable dependency, or should we write our own?

UTF-8 encoding not working

UTF-8 encoding not working on my end every time I send and error/ exception it removes all special characters

Continue logging errors locally to error_log

The default usage in the Readme disables the local error_log file. We'd like to send errors to Raygun but also send them to the local error_log. Any recommendations or examples on the best way to do this? Be great to add it to the Readme as well.

Errors sent with SendError don't look similar to SendException

The SendError is the one that uses the inbuilt error_get_last() function that returns an array not an exception.

The SendError code then tries to create an ErrorException from the error details on https://github.com/MindscapeHQ/raygun4php/blob/master/src/Raygun4php/RaygunClient.php#L85. This loses the stack trace and the original stack trace is included in the message as the error_get_last()['message'] is the full stack trace converted to a new line delimited string.

SendException seems to send the error in a format that includes the stack trace.

Incorrect no. params passed to ErrorException

PHP 5.5.9 / Ubuntu 14.04 / RayGun4PHP dev-master (9252af2) used in SilverStripe's own RayGun module.

PHP's ErrorException class's constructor expects 6 params, but RayGunClient sends only 5 via a call to BuildMessage() in RayGunClient.php:74, which, ironically is reported within raygun.io itself :-P

json_encode() trouble with unicode

The json_encode() function has trouble when passed non-unicode data. This sometimes happens due to malformed user input. This results in the RaygunClient#send() method throwing an additional error (json_encode(): Invalid UTF-8 sequence in argument).

This can be fixed by changing line 143 of RaygunClient.php to:

curl_setopt($httpData, CURLOPT_POSTFIELDS, json_encode(iconv('UTF-8', 'UTF-8//IGNORE', $message)));

Issues when bundled in a phar archive

First issue: realpath() usage does not work with phar, when called on __DIR__. I have been running a fork with the following fix to get the class loading to work: halkyon@d74d176
Recommended approach would be to go all in with composer PSR-4 autoloading, so these require statements can be removed entirely. This would break the ability to install raygun4php without composer, but I suspect it's not a popular way to install this client.

Second issue: async sending doesn't work, because RaygunClient shells out to curl and passes a cert path like phar:///path/to/cert.crt which won't work because references to files inside a phar are "virtual" paths, not real filesystem ones. Workaround has to been to set useAsyncSending to false which works, because it uses php stream functions that support phar paths. A recommended approach for portability would be to replace the HTTP call code with Guzzle, which also supports sending async.

Exclusion of sensitive data

I have a problem. The main Raygun's documentation contains one magic sentence. "Note that when any filters are defined, the Raygun error will no longer contain the raw HTTP data, since there's no effective way to filter it.". In this situation, I wonder how I can encrypt selected sensitive data and transfer the necessary data from rawData? For each registered error I need to know what request flew to my api. However, encryption makes me not able to see it. Is there any way to do this?

Requires cookies for affected user monitoring, even when RUM isn't wanted

Why can't I use affected user monitoring without a bunch of cookie headers being emitted? Is it because the user identifier is also passed to Pulse? If so, there should be a way to set $this->user on the client to a RaygunIdentifier without emitting the cookies, for people who only want Crash Reporting + Affected users (and not Pulse RUM).

If I have my own state storage (like sessions, like 99% of web apps), why do I need to carry additional state through every HTTP request, just to support a user identifier being supplied to error ingestion?

escapeshellarg(): Input string contains NULL bytes

Hi I'm getting the following error message every seconds for the past hour.
Seems like you should escape the string maybe?

#0 {main}   
Apr 12 19:00:47 lucky app/worker.1:    [Symfony\Component\Debug\Exception\FatalErrorException]   
Apr 12 19:00:47 lucky app/worker.1:    escapeshellarg(): Input string contains NULL bytes        
Apr 12 19:00:49 lucky app/worker.1:  PHP Fatal error:  escapeshellarg(): Input string contains NULL bytes in /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php on line 310 
Apr 12 19:00:49 lucky app/worker.1:  [2016-04-12 19:00:48] production.ERROR: Symfony\Component\Debug\Exception\FatalErrorException: escapeshellarg(): Input string contains NULL bytes in /app/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php:310 

Version Composer Packages

I see you all are using versions properly for feature releases but aren't taking advantage of them for composer.

It would be nice if instead of using dev-master I could say 1.* or something like it. Not sure if you all are familiar with http://semver.org/ but it would be a good place to start.

I think you just have to tag the release in git for it to be addressable in this manner.

Thanks!

Does Not Report Actual Error Line Number & File

I was testing Raygun out for PHP, but I noticed that it doesn't actually report the file name and line number of the actual error, which is the most important part.

I am just manually doing a $raygunClient->SendException($exception) and I get a something like this:

screen shot 2015-09-05 at 12 05 10 pm

The error was on ProductController.php:39, something I know cuz 1) I put the error there and 2) if I log $exception->getFile() and $exception->getLine() (or it's even reported in $exception->getTraceAsString()), I can find this information.

Am I doing something wrong, or would it be possible for this information to be added to the error reports?

Here's the relevant part of the raw data (which does have the file, but it isn't displayed in any of the "pretty views" but no line number):

{
     "Error": {
          "Message": "Symfony\\Component\\Debug\\Exception\\FatalErrorException: Call to undefined function GoDirectFoods\\Http\\Controllers\\foo()",
          "ClassName": "Symfony\\Component\\Debug\\Exception\\FatalErrorException",
          "StackTrace": [
               {
                    "LineNumber": 131,
                    "ClassName": "Symfony\\Component\\Debug\\Exception\\FatalErrorException",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php",
                    "MethodName": "__construct"
               },
               {
                    "LineNumber": 116,
                    "ClassName": "Illuminate\\Foundation\\Bootstrap\\HandleExceptions",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php",
                    "MethodName": "fatalExceptionFromError"
               },
               {
                    "LineNumber": 0,
                    "ClassName": "Illuminate\\Foundation\\Bootstrap\\HandleExceptions",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php",
                    "MethodName": "handleShutdown"
               },
               {
                    "LineNumber": 246,
                    "ClassName": "GoDirectFoods\\Http\\Controllers\\ProductController",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
                    "MethodName": "index"
               },
               {
                    "LineNumber": 246,
                    "ClassName": "",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
                    "MethodName": "call_user_func_array:{/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Controller.php:246}"
               },
               {
                    "LineNumber": 162,
                    "ClassName": "Illuminate\\Routing\\Controller",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php",
                    "MethodName": "callAction"
               },
               {
                    "LineNumber": 107,
                    "ClassName": "Illuminate\\Routing\\ControllerDispatcher",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php",
                    "MethodName": "call"
               },
               {
                    "LineNumber": 141,
                    "ClassName": "Illuminate\\Routing\\ControllerDispatcher",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "Illuminate\\Routing\\{closure}"
               },
               {
                    "LineNumber": 141,
                    "ClassName": "",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "call_user_func:{/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:141}"
               },
               {
                    "LineNumber": 101,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 101,
                    "ClassName": "",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "call_user_func:{/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:101}"
               },
               {
                    "LineNumber": 108,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php",
                    "MethodName": "then"
               },
               {
                    "LineNumber": 67,
                    "ClassName": "Illuminate\\Routing\\ControllerDispatcher",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php",
                    "MethodName": "callWithinStack"
               },
               {
                    "LineNumber": 204,
                    "ClassName": "Illuminate\\Routing\\ControllerDispatcher",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Route.php",
                    "MethodName": "dispatch"
               },
               {
                    "LineNumber": 134,
                    "ClassName": "Illuminate\\Routing\\Route",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Route.php",
                    "MethodName": "runWithCustomDispatcher"
               },
               {
                    "LineNumber": 701,
                    "ClassName": "Illuminate\\Routing\\Route",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
                    "MethodName": "run"
               },
               {
                    "LineNumber": 141,
                    "ClassName": "Illuminate\\Routing\\Router",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "Illuminate\\Routing\\{closure}"
               },
               {
                    "LineNumber": 141,
                    "ClassName": "",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "call_user_func:{/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:141}"
               },
               {
                    "LineNumber": 32,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/app/Http/Middleware/DatabaseTransactions.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 451,
                    "ClassName": "GoDirectFoods\\Http\\Middleware\\DatabaseTransactions",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Database/Connection.php",
                    "MethodName": "GoDirectFoods\\Http\\Middleware\\{closure}"
               },
               {
                    "LineNumber": 33,
                    "ClassName": "Illuminate\\Database\\Connection",
                    "FileName": "/home/godirectfoods/app/Http/Middleware/DatabaseTransactions.php",
                    "MethodName": "transaction"
               },
               {
                    "LineNumber": 125,
                    "ClassName": "GoDirectFoods\\Http\\Middleware\\DatabaseTransactions",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 101,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 101,
                    "ClassName": "",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "call_user_func:{/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:101}"
               },
               {
                    "LineNumber": 703,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
                    "MethodName": "then"
               },
               {
                    "LineNumber": 670,
                    "ClassName": "Illuminate\\Routing\\Router",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
                    "MethodName": "runRouteWithinStack"
               },
               {
                    "LineNumber": 628,
                    "ClassName": "Illuminate\\Routing\\Router",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
                    "MethodName": "dispatchToRoute"
               },
               {
                    "LineNumber": 214,
                    "ClassName": "Illuminate\\Routing\\Router",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
                    "MethodName": "dispatch"
               },
               {
                    "LineNumber": 141,
                    "ClassName": "Illuminate\\Foundation\\Http\\Kernel",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "Illuminate\\Foundation\\Http\\{closure}"
               },
               {
                    "LineNumber": 141,
                    "ClassName": "",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "call_user_func:{/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:141}"
               },
               {
                    "LineNumber": 48,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/app/Http/Middleware/Timezone.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 125,
                    "ClassName": "GoDirectFoods\\Http\\Middleware\\Timezone",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 43,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 125,
                    "ClassName": "Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 55,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 125,
                    "ClassName": "Illuminate\\View\\Middleware\\ShareErrorsFromSession",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 61,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 125,
                    "ClassName": "Illuminate\\Session\\Middleware\\StartSession",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 36,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 125,
                    "ClassName": "Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 40,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 125,
                    "ClassName": "Illuminate\\Cookie\\Middleware\\EncryptCookies",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 42,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 125,
                    "ClassName": "Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 101,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "Illuminate\\Pipeline\\{closure}"
               },
               {
                    "LineNumber": 101,
                    "ClassName": "",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
                    "MethodName": "call_user_func:{/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:101}"
               },
               {
                    "LineNumber": 115,
                    "ClassName": "Illuminate\\Pipeline\\Pipeline",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
                    "MethodName": "then"
               },
               {
                    "LineNumber": 84,
                    "ClassName": "Illuminate\\Foundation\\Http\\Kernel",
                    "FileName": "/home/godirectfoods/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
                    "MethodName": "sendRequestThroughRouter"
               },
               {
                    "LineNumber": 54,
                    "ClassName": "Illuminate\\Foundation\\Http\\Kernel",
                    "FileName": "/home/godirectfoods/public/index.php",
                    "MethodName": "handle"
               },
               {
                    "LineNumber": 0,
                    "ClassName": "",
                    "FileName": "/home/godirectfoods/public/index.php",
                    "MethodName": "{main}"
               }
          ],
          "FileName": "ProductController.php",
          "Data": null
     }
}

PHP_AUTH_USER and PHP_AUTH_PW (BasicAuth credentials) being sent and stored in Raygun dashboard

Hey team,

If the $_SERVER superglobal has PHP_AUTH_USER and/or PHP_AUTH_PW variables defined (because a user has logged in using basic auth), they get included and passed through to the Raygun dashboard.

These should be stripped out by default I think, perhaps with an optional config flag to allow users to not filter these (e.g. for cases where you need to store that information to replay requests correctly, although I don't think you should ever allow PHP_AUTH_PW to be stored).

This would probably be fixed by #32, and could form part of a default blacklist of fields.

Async auto-off on windows?

The docs state "For Windows, false is the only effective option available due to a bug in SSL sending on IIS with certain versions of PHP. Passing in true will do nothing on this platform."

But I run Apache on my local Windows machine not IIS. Is this bug an IIS bug or a Windows bug? (the code check the OS string, not the web server)

I use lots of SSL api calls from my code so I know SSL works on Windows + Apache.

I haven't tried Raygun yet. I'd just like some clarification on the nature of the bug (I'm currently exploring error loggers). Thanks.

"Couldn't send asynchronously" error

Hi team,

Occasionnaly, our PHP service is wrapping errors with:

<br/><br/><strong>Raygun Warning:</strong> Couldn't send asynchronously. Try calling new RaygunClient('apikey', FALSE); to use an alternate sending method, or RaygunClient('key', FALSE, TRUE) to echo the HTTP response<br/><br/>
  1. Do you know why this is occurring occasionally? We are sending our errors asynchronously to Raygun and would like to continue to do so. It looks like a socket connection couldn't be opened, but that's all we know...

  2. Because of this wrapping from our PHP service, our Go service getting the errors is unable to parse them (it expect some JSON: Failed to unmarshal json: invalid character '<' looking for beginning of value). Is there a way to avoid this behaviour?

Thanks for your help!

disable user tracking cookies

There should be a way to completely disable all user tracking cookies.

All that is needed is to not call SetUser() in the client constructor.

image

image

Exclude all form data

From https://raygun.com/thinktank/suggestion/9246

It looks like this is available for .net projects but not for php projects (why do the different language SDKs offer different options in the first place?)

It would be great if the PHP SDK could have the ability to filter out all form submission data without having to specify every possible form field individually.

iconv(): Detected an illegal character in input string

We've started to get a few of these errors come through. This is the stack trace we're seeing:

#0 iconv(): Detected an illegal character in input string:: called at [/home/example/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php:414]
#1 DDC\Error::logToRaygun called at [/home/example/vendor/drugscom/error/src/DDC/error.php:367]
#2 DDC\Error::log called at [/home/example/vendor/drugscom/error/src/DDC/error.php:190]
#3 DDC\Error::customErrorHandler called at [:0]
#4 ::iconv called at [/home/example/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php:414]
#5 Raygun4php\RaygunClient::Raygun4php\{closure} called at [:0]
#6 ::preg_replace_callback called at [/home/example/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php:414]
#7 Raygun4php\RaygunClient::toJsonRemoveUnicodeSequences called at [/home/example/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php:324]
#8 Raygun4php\RaygunClient::Send called at [/home/example/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php:99]
#9 iconv(): Detected an illegal character in input string:: called at [/home/example/vendor/mindscape/raygun4php/src/Raygun4php/RaygunClient.php:414]
#10 DDC\Error::logToRaygun called at [/home/example/vendor/drugscom/error/src/DDC/error.php:367]
#11 DDC\Error::log called at [/home/example/vendor/drugscom/error/src/DDC/error.php:137]
#12 DDC\Error::customErrorShutdown called at [:0]

Whitelist/Blacklist form fields

Just wondering if there is any thought on whitelisting/blacklisting certain form field names (i.e. passwords) so they are not transmitted during an exception?

Add error bundling to help deal with high traffic

Original pull request from #93 had the following points:

Updates

  • Add a bundler class which populates an array with incoming messages
  • Send a bundle after certain conditions have been met. Either the number of messages in the bundle reaches the maximum bundle size (default 100) or after a time period (default 60s)
  • Post bundle to the bundle entries endpoint on the API

@samuel-holt said on the PR: I would be interested in adding this bundling functionality but with a different approach, I think this PR is stale and should be deleted.

This issue can track a future enhancement to re-implement it again.

Setting cookies when headers sent

Recently Raygun started tracking users by setting unique id cookies. The problem is that sometimes an error occurs after output has already started (e.g., if the error is triggered in a template). This results in another error being triggered, since you can't set cookies after output has been sent to the browser.

A pretty easy fix would be to add a conditional:

if(!headers_sent()) 

in RaygunClient.php before calling setcookie().

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.