Completed
Push — master ( 3c4d1c...8994f7 )
by Russell
02:23
created

SentryLogWriter::getPackageInfo()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 4
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 23 and the first side effect is on line 12.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 * Class: SentryLogWriter.
5
 *
6
 * @author  Russell Michell 2017 <[email protected]>
7
 * @package phptek/sentry
8
 */
9
10
namespace phptek\Sentry;
11
12
require_once THIRDPARTY_PATH . '/Zend/Log/Writer/Abstract.php';
13
14
/**
15
 * The SentryLogWriter class simply acts as a bridge between the configured Sentry 
16
 * adaptor and SilverStripe's {@link SS_Log}.
17
 * 
18
 * Usage in your project's _config.php for example (See README for examples).
19
 *  
20
 *    SS_Log::add_writer(\phptek\Sentry\SentryLogWriter::factory(), '<=');
21
 */
22
23
class SentryLogWriter extends \Zend_Log_Writer_Abstract
24
{
25
    
26
    /**
27
     * Stipulates what gets shown in the Sentry UI, should some metric not be
28
     * available for any reason.
29
     *
30
     * @const string
31
     */
32
    const SLW_NOOP = 'Unavailable';
33
    
34
    /**
35
     * A static constructor as per {@link Zend_Log_FactoryInterface}.
36
     * 
37
     * @param  array $config    An array of optional additional configuration for
38
     *                          passing custom information to Sentry. See the README for more detail.
39
     * @return SentryLogWriter
40
     */
41
    public static function factory($config = [])
42
    {
43
        $env = isset($config['env']) ? $config['env'] : null;
44
        $tags = isset($config['tags']) ? $config['tags'] : [];
45
        $extra = isset($config['extra']) ? $config['extra'] : [];
46
        $writer = \Injector::inst()->get('SentryLogWriter');
47
48
        // Set default environment
49
        if (is_null($env)) {
50
            $env = $writer->defaultEnv();
51
        }
52
53
        // Set all available user-data
54
        $userData = $writer->defaultUserData();
55
        
56
        if ($member = \Member::currentUser()) {
57
            $userData = $writer->defaultUserData($member);
58
        }
59
60
        // Set any available tags available in SS config
61
        $tags = array_merge($writer->defaultTags(), $tags);
62
63
        // Set any avalable additional (extra) data
64
        $extra = array_merge($writer->defaultExtra(), $extra);
65
66
        $writer->client->setData('env', $env);
67
        $writer->client->setData('user', $userData);
68
        $writer->client->setData('tags', $tags);
69
        $writer->client->setData('extra', $extra);
70
71
        return $writer;
72
    }
73
74
    /**
75
     * Used in unit tests.
76
     *
77
     * @return SentryClientAdaptor
78
     */
79
    public function getClient()
80
    {
81
        return $this->client;
0 ignored issues
show
Bug introduced by
The property client does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
82
    }
83
84
    /**
85
     * Returns a default environment when one isn't passed to the factory()
86
     * method.
87
     *
88
     * @return string
89
     */
90
    public function defaultEnv()
91
    {
92
        return \Director::get_environment_type();
93
    }
94
    
95
    /**
96
     * Returns a default set of additional data specific to the user's part in
97
     * the request.
98
     * 
99
     * @param  Member $member
100
     * @return array
101
     */
102
    public function defaultUserData(\Member $member = null)
103
    {
104
        return [
105
            'IP-Address'    => $this->getIP(),
106
            'ID'            => $member ? $member->getField('ID') : self::SLW_NOOP,
107
            'Email'         => $member ? $member->getField('Email') : self::SLW_NOOP,
108
        ];
109
    }
110
    
111
    /**
112
     * Returns a default set of additional "tags" we wish to send to Sentry.
113
     * By default, Sentry reports on several mertrics, and we're already sending 
114
     * {@link Member} data. But there are additional data that would be useful
115
     * for debugging via the Sentry UI.
116
     *
117
     * These data can augment that which is sent to Sentry at setup
118
     * time in _config.php. See the README for more detail.
119
     *
120
     * N.b. Tags can be used to group messages within the Sentry UI itself, so there
121
     * should only be "static" data being sent, not something that can drastically
122
     * or minutely change, such as memory usage for example.
123
     * 
124
     * @return array
125
     */
126
    public function defaultTags()
127
    {
128
        return [
129
            'Request-Method'=> $this->getReqMethod(),
130
            'Request-Type'  => $this->getRequestType(),
131
            'SAPI'          => $this->getSAPI(),
132
            'SS-Version'    => $this->getPackageInfo('silverstripe/framework')
133
        ];
134
    }
135
136
    /**
137
     * Returns a default set of extra data to show upon selecting a message for
138
     * analysis in the Sentry UI. This can augment the data sent to Sentry at setup
139
     * time in _config.php as well as at runtime when calling SS_Log itself.
140
     * See the README for more detail.
141
     *
142
     * @return array
143
     */
144
    public function defaultExtra()
145
    {
146
        return [
147
            'Peak-Memory'   => $this->getPeakMemory()
148
        ];
149
    }
150
    
151
    /**
152
     * _write() forms the entry point into the physical sending of the error. The 
153
     * sending itself is done by the current adaptor's `send()` method.
154
     * 
155
     * @param  array $event An array of data that is created in, and arrives here
156
     *                      via {@link SS_Log::log()} and {@link Zend_Log::log}.
157
     * @return void
158
     */
159
    protected function _write($event)
160
    {
161
        $message = $event['message']['errstr'];                             // From SS_Log::log()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
162
        // The complete compliment of these data come via the Raven_Client::xxx_context() methods
163
        $data = [
164
            'timestamp' => strtotime($event['timestamp']),                  // From Zend_Log::log()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
165
            'extra'     => isset($event['extra']) ? $event['extra'] : ''    // From _config.php (Optional)
166
        ];
167
        $trace = \SS_Backtrace::filter_backtrace(debug_backtrace(), ['SentryLogWriter->_write']);
168
        
169
        $this->client->send($message, [], $data, $trace);
170
    }
171
    
172
    /**
173
     * Return the version of $pkg taken from composer.lock.
174
     * 
175
     * @param  string $pkg e.g. "silverstripe/framework"
176
     * @return string
177
     */
178
    public function getPackageInfo($pkg)
179
    {
180
        $lockFileJSON = BASE_PATH . '/composer.lock';
181
182
        if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) {
183
            return self::SLW_NOOP;
184
        }
185
186
        $lockFileData = json_decode(file_get_contents($lockFileJSON), true);
187
188
        foreach ($lockFileData['packages'] as $package) {
189
            if ($package['name'] === $pkg) {
190
                return $package['version'];
191
            }
192
        }
193
        
194
        return self::SLW_NOOP;
195
    }
196
    
197
    /**
198
     * Return the IP address of the relevant request.
199
     * 
200
     * @return string
201
     */
202
    public function getIP()
203
    {
204
        $req = \Injector::inst()->create('SS_HTTPRequest', $this->getReqMethod(), '');
205
        
206
        if ($ip = $req->getIP()) {
207
            return $ip;
208
        }
209
        
210
        return self::SLW_NOOP;
211
    }
212
    
213
    /**
214
     * What sort of request is this? (A harder question to answer than you might
215
     * think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request)
216
     * 
217
     * @return string
218
     */
219
    public function getRequestType()
220
    {
221
        $isCLI = $this->getSAPI() !== 'cli';
222
        $isAjax = \Director::is_ajax();
223
224
        return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax';
225
    }
226
    
227
    /**
228
     * Return peak memory usage.
229
     * 
230
     * @return float
231
     */
232
    public function getPeakMemory()
233
    {
234
        $peak = memory_get_peak_usage(true) / 1024 / 1024;
235
        
236
        return (string) round($peak, 2) . 'Mb';
237
    }
238
    
239
    /**
240
     * Basic User-Agent check and return.
241
     * 
242
     * @return string
243
     */
244
    public function getUserAgent()
0 ignored issues
show
Coding Style introduced by
getUserAgent uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
245
    {
246
        $ua = @$_SERVER['HTTP_USER_AGENT'];
247
        
248
        if (!empty($ua)) {
249
            return $ua;
250
        }
251
        
252
        return self::SLW_NOOP;
253
    }
254
    
255
    /**
256
     * Basic reuqest method check and return.
257
     * 
258
     * @return string
259
     */
260
    public function getReqMethod()
0 ignored issues
show
Coding Style introduced by
getReqMethod uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
261
    {
262
        $method = @$_SERVER['REQUEST_METHOD'];
263
        
264
        if (!empty($method)) {
265
            return $method;
266
        }
267
        
268
        return self::SLW_NOOP;
269
    }
270
    
271
    /**
272
     * @return string
273
     */
274
    public function getSAPI()
275
    {
276
        return php_sapi_name();
277
    }
278
    
279
}
280