SentryTarget   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 238
Duplicated Lines 0 %

Importance

Changes 5
Bugs 3 Features 0
Metric Value
eloc 75
c 5
b 3
f 0
dl 0
loc 238
rs 9.84
wmc 32

12 Methods

Rating   Name   Duplication   Size   Complexity  
A convertLevel() 0 3 2
A setScopeExtras() 0 4 1
B setScopeUser() 0 12 7
A clearScope() 0 4 1
A export() 0 20 3
A setExtraContext() 0 8 2
A setScopeTags() 0 4 1
A runExtraCallback() 0 11 4
A setScopeLevel() 0 6 1
A getRelease() 0 3 1
B convertMessage() 0 23 7
A init() 0 15 2
1
<?php
2
3
namespace asminog\yii2sentry;
4
5
use Exception;
6
use Sentry\Severity;
7
use Sentry\State\Scope;
8
use Throwable;
9
use Yii;
10
use yii\base\InvalidConfigException;
11
use yii\helpers\ArrayHelper;
12
use yii\helpers\VarDumper;
13
use yii\log\Logger;
14
use yii\log\Target;
15
use function Sentry\captureException;
16
use function Sentry\captureMessage;
17
use function Sentry\configureScope;
18
use function Sentry\init;
19
20
/**
21
 * SentryTarget send logs to Sentry.
22
 *
23
 * @see https://sentry.io
24
 *
25
 * @property array $scopeExtras
26
 * @property int $scopeLevel
27
 * @property array $scopeTags
28
 * @property string $contextMessage
29
 */
30
class SentryTarget extends Target
31
{
32
    /**
33
     * @var string DNS for sentry.
34
     */
35
    public string $dsn;
36
37
    /**
38
     * @var string Release for sentry.
39
     */
40
    public string $release = 'auto';
41
42
    /**
43
     * @var array Options for sentry.
44
     */
45
    public array $options = [];
46
47
    /**
48
     * @var array UserIdentity attributes for user context.
49
     */
50
    public array $collectUserAttributes = ['id', 'username', 'email'];
51
52
    /**
53
     * @var array Allow to collect context automatically.
54
     */
55
    public array $collectContext = ['_SESSION', 'argv'];
56
57
    /**
58
     * @var callable Callback function that can modify extra's array
59
     */
60
    public $extraCallback;
61
62
    const SENTRY_LEVELS = [
63
        Logger::LEVEL_ERROR => Severity::ERROR,
64
        Logger::LEVEL_WARNING => Severity::WARNING,
65
        Logger::LEVEL_INFO => Severity::INFO,
66
        Logger::LEVEL_TRACE => Severity::DEBUG,
67
        Logger::LEVEL_PROFILE_BEGIN => Severity::DEBUG,
68
        Logger::LEVEL_PROFILE_END => Severity::DEBUG,
69
        Logger::LEVEL_PROFILE => Severity::DEBUG,
70
    ];
71
72
73
    /**
74
     * @inheritdoc
75
     */
76
    public function init()
77
    {
78
        $this->logVars = [];
79
80
        init(
81
            array_merge(
82
                $this->options,
83
                [
84
                    'dsn' => $this->dsn,
85
                    'release' => ($this->release == 'auto' ? $this->getRelease() : $this->release)
86
                ]
87
            )
88
        );
89
90
        parent::init();
91
    }
92
93
    /**
94
     * Get release based on git information
95
     * @return string
96
     */
97
    private function getRelease(): string
98
    {
99
        return trim(exec('test git --version 2>&1 >/dev/null && git log --pretty="%H" -n1 HEAD || echo "trunk"'));
100
    }
101
102
    /**
103
     * @inheritdoc
104
     * @throws InvalidConfigException
105
     */
106
    public function export()
107
    {
108
        foreach ($this->messages as $message) {
109
            [$message, $level, $category] = $message;
110
111
            $this->clearScope();
112
            $this->setScopeUser();
113
            $this->setExtraContext();
114
            $this->setScopeLevel($level);
115
            $this->setScopeTags(['category' => $category]);
116
117
            if ($message instanceof Throwable) {
118
                $this->setScopeExtras($this->runExtraCallback($message));
119
                captureException($message);
120
                continue;
121
            }
122
123
            $message = $this->convertMessage($message);
124
125
            captureMessage($message);
126
        }
127
    }
128
129
    /**
130
     * Set sentry level scope based on yii2 level message
131
     *
132
     * @param int $level
133
     */
134
    private function setScopeLevel(int $level)
135
    {
136
        $level = $this->convertLevel($level);
137
138
        configureScope(function (Scope $scope) use ($level) : void {
139
            $scope->setLevel($level);
140
        });
141
    }
142
143
    /**
144
     * Convert Logger level to Sentry level
145
     * @param int $level
146
     * @return Severity
147
     */
148
    private function convertLevel(int $level): Severity
149
    {
150
        return isset(self::SENTRY_LEVELS[$level]) ? new Severity(self::SENTRY_LEVELS[$level]) : Severity::fatal();
151
    }
152
153
    /**
154
     * Set sentry user scope based on yii2 Yii::$app->user
155
     * @throws InvalidConfigException
156
     * @throws Exception
157
     */
158
    private function setScopeUser()
159
    {
160
        if (session_status() === PHP_SESSION_ACTIVE and Yii::$app->get('user', false) !== null and Yii::$app->user->getId() and !empty($this->collectUserAttributes)) {
0 ignored issues
show
Bug introduced by
The method getId() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

160
        if (session_status() === PHP_SESSION_ACTIVE and Yii::$app->get('user', false) !== null and Yii::$app->user->/** @scrutinizer ignore-call */ getId() and !empty($this->collectUserAttributes)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
161
            $attributes = ['id' => Yii::$app->user->getId()];
162
            if (($user = Yii::$app->user->identity) !== null) {
163
                foreach ($this->collectUserAttributes as $collectUserAttribute) {
164
                    $attributes[$collectUserAttribute] = ArrayHelper::getValue($user, $collectUserAttribute);
165
                }
166
            }
167
168
            configureScope(function (Scope $scope) use ($attributes): void {
169
                $scope->setUser($attributes);
170
            });
171
        }
172
    }
173
174
    /**
175
     * Set sentry scope tags
176
     *
177
     * @param array $tags
178
     */
179
    private function setScopeTags(array $tags)
180
    {
181
        configureScope(function (Scope $scope) use ($tags): void {
182
            $scope->setTags($tags);
183
        });
184
    }
185
186
    /**
187
     * Add extra context if needed
188
     */
189
    private function setExtraContext()
190
    {
191
        if (!empty($this->collectContext)) {
192
            $this->logVars = $this->collectContext;
193
            $extraContext = $this->getContextMessage();
194
195
            $this->setScopeExtras(['CONTEXT' => $extraContext]);
196
            $this->logVars = [];
197
        }
198
    }
199
200
    /**
201
     * Set sentry scope extras
202
     * @param array $extras
203
     */
204
    private function setScopeExtras(array $extras)
205
    {
206
        configureScope(function (Scope $scope) use ($extras): void {
207
            $scope->setExtras($extras);
208
        });
209
    }
210
211
    /**
212
     * Calls the extra user callback if it exists
213
     *
214
     * @param mixed $message log message from Logger::messages
215
     * @param array $extra
216
     * @return array
217
     */
218
    protected function runExtraCallback($message, array $extra = []): array
219
    {
220
        if ($this->extraCallback and is_callable($this->extraCallback)) {
221
            $extra = call_user_func($this->extraCallback, $message, $extra);
222
        }
223
224
        if (!is_array($extra)) {
225
            $extra = ['extra' => VarDumper::dumpAsString($extra)];
226
        }
227
228
        return $extra;
229
    }
230
231
    /**
232
     * Clear sentry scope for new message
233
     */
234
    private function clearScope()
235
    {
236
        configureScope(function (Scope $scope): void {
237
            $scope->clear();
238
        });
239
    }
240
241
    /**
242
     * @param $message
243
     * @return string
244
     */
245
    private function convertMessage($message): string
246
    {
247
        if (is_array($message)) {
248
            if (isset($message['tags'])) {
249
                $this->setScopeTags($message['tags']);
250
                unset($message['tags']);
251
            }
252
253
            if (isset($message['extra'])) {
254
                $this->setScopeExtras($this->runExtraCallback($message, $message['extra']));
255
                unset($message['extra']);
256
            }
257
258
            if (count($message) == 1 and isset($message['msg'])) {
259
                $message = $message['msg'];
260
            }
261
        }
262
263
        if (!is_string($message)) {
264
            $message = VarDumper::dumpAsString($message);
265
        }
266
267
        return $message;
268
    }
269
}
270