-
-
Notifications
You must be signed in to change notification settings - Fork 468
Expand file tree
/
Copy pathBufferCaptureStrategy.kt
More file actions
249 lines (225 loc) · 8.05 KB
/
BufferCaptureStrategy.kt
File metadata and controls
249 lines (225 loc) · 8.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
package io.sentry.android.replay.capture
import android.annotation.TargetApi
import android.graphics.Bitmap
import android.view.MotionEvent
import io.sentry.DateUtils
import io.sentry.IScopes
import io.sentry.SentryLevel.DEBUG
import io.sentry.SentryLevel.ERROR
import io.sentry.SentryLevel.INFO
import io.sentry.SentryOptions
import io.sentry.SentryReplayEvent.ReplayType.BUFFER
import io.sentry.SentryReplayEvent.ReplayType.SESSION
import io.sentry.android.replay.ReplayCache
import io.sentry.android.replay.ScreenshotRecorderConfig
import io.sentry.android.replay.capture.CaptureStrategy.Companion.rotateEvents
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
import io.sentry.android.replay.util.sample
import io.sentry.android.replay.util.submitSafely
import io.sentry.protocol.SentryId
import io.sentry.transport.ICurrentDateProvider
import io.sentry.util.FileUtils
import io.sentry.util.Random
import java.io.File
import java.util.Date
import java.util.concurrent.ScheduledExecutorService
@TargetApi(26)
internal class BufferCaptureStrategy(
private val options: SentryOptions,
private val scopes: IScopes?,
private val dateProvider: ICurrentDateProvider,
private val random: Random,
executor: ScheduledExecutorService,
replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null,
) :
BaseCaptureStrategy(
options,
scopes,
dateProvider,
executor,
replayCacheProvider = replayCacheProvider,
) {
// TODO: capture envelopes for buffered segments instead, but don't send them until buffer is
// triggered
private val bufferedSegments = mutableListOf<ReplaySegment.Created>()
internal companion object {
private const val TAG = "BufferCaptureStrategy"
private const val ENVELOPE_PROCESSING_DELAY: Long = 100L
}
override fun pause() {
createCurrentSegment("pause") { segment ->
if (segment is ReplaySegment.Created) {
bufferedSegments += segment
currentSegment++
}
}
super.pause()
}
override fun stop() {
val replayCacheDir = cache?.replayCacheDir
replayExecutor.submitSafely(options, "$TAG.stop") {
FileUtils.deleteRecursively(replayCacheDir)
currentSegment = -1
}
super.stop()
}
override fun captureReplay(isTerminating: Boolean, onSegmentSent: (Date) -> Unit) {
val sampled = random.sample(options.sessionReplay.onErrorSampleRate)
if (!sampled) {
options.logger.log(
INFO,
"Replay wasn't sampled by onErrorSampleRate, not capturing for event",
)
return
}
// write replayId to scope right away, so it gets picked up by the event that caused buffer
// to flush
scopes?.configureScope {
it.replayId = currentReplayId
it.replayType = replayType
}
if (isTerminating) {
this.isTerminating.set(true)
// avoid capturing replay, because the video will be malformed
options.logger.log(
DEBUG,
"Not capturing replay for crashed event, will be captured on next launch",
)
return
}
createCurrentSegment("capture_replay") { segment ->
bufferedSegments.capture()
if (segment is ReplaySegment.Created) {
segment.capture(scopes)
// we only want to increment segment_id in the case of success, but currentSegment
// might be irrelevant since we changed strategies, so in the callback we increment
// it on the new strategy already
onSegmentSent(segment.replay.timestamp)
}
}
}
override fun onScreenshotRecorded(
bitmap: Bitmap?,
store: ReplayCache.(frameTimestamp: Long) -> Unit,
) {
// have to do it before submitting, otherwise if the queue is busy, the timestamp won't be
// reflecting the exact time of when it was captured
val frameTimestamp = dateProvider.currentTimeMillis
replayExecutor.submitSafely(options, "$TAG.add_frame") {
cache?.store(frameTimestamp)
val now = dateProvider.currentTimeMillis
val bufferLimit = now - options.sessionReplay.errorReplayDuration
screenAtStart = cache?.rotate(bufferLimit)
bufferedSegments.rotate(bufferLimit)
}
}
override fun onConfigurationChanged(recorderConfig: ScreenshotRecorderConfig) {
createCurrentSegment("configuration_changed") { segment ->
if (segment is ReplaySegment.Created) {
bufferedSegments += segment
currentSegment++
}
}
super.onConfigurationChanged(recorderConfig)
}
override fun convert(): CaptureStrategy {
if (isTerminating.get()) {
options.logger.log(
DEBUG,
"Not converting to session mode, because the process is about to terminate",
)
return this
}
// we hand over replayExecutor to the new strategy to preserve order of execution
val captureStrategy = SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor)
captureStrategy.recorderConfig = recorderConfig
captureStrategy.start(
segmentId = currentSegment,
replayId = currentReplayId,
replayType = BUFFER,
)
// The type on the scope should change, as logs read it
scopes?.configureScope { it.replayType = SESSION }
return captureStrategy
}
override fun onTouchEvent(event: MotionEvent) {
super.onTouchEvent(event)
val bufferLimit = dateProvider.currentTimeMillis - options.sessionReplay.errorReplayDuration
rotateEvents(currentEvents, bufferLimit)
}
private fun deleteFile(file: File?) {
if (file == null) {
return
}
try {
if (!file.delete()) {
options.logger.log(ERROR, "Failed to delete replay segment: %s", file.absolutePath)
}
} catch (e: Throwable) {
options.logger.log(ERROR, e, "Failed to delete replay segment: %s", file.absolutePath)
}
}
private fun MutableList<ReplaySegment.Created>.capture() {
var bufferedSegment = removeFirstOrNull()
while (bufferedSegment != null) {
bufferedSegment.capture(scopes)
bufferedSegment = removeFirstOrNull()
// a short delay between processing envelopes to avoid bursting our server and hitting
// another rate limit https://develop.sentry.dev/sdk/features/#additional-capabilities
// InterruptedException will be handled by the outer try-catch
Thread.sleep(ENVELOPE_PROCESSING_DELAY)
}
}
private fun MutableList<ReplaySegment.Created>.rotate(bufferLimit: Long) {
// TODO: can be a single while-loop
var removed = false
removeAll {
// it can be that the buffered segment is half-way older than the buffer limit, but
// we only drop it if its end timestamp is older
if (it.replay.timestamp.time < bufferLimit) {
currentSegment--
deleteFile(it.replay.videoFile)
removed = true
return@removeAll true
}
return@removeAll false
}
if (removed) {
// shift segmentIds after rotating buffered segments
forEachIndexed { index, segment -> segment.setSegmentId(index) }
}
}
private fun createCurrentSegment(taskName: String, onSegmentCreated: (ReplaySegment) -> Unit) {
val currentConfig = recorderConfig
if (currentConfig == null) {
options.logger.log(
DEBUG,
"Recorder config is not set, not creating segment for task: $taskName",
)
return
}
val errorReplayDuration = options.sessionReplay.errorReplayDuration
val now = dateProvider.currentTimeMillis
val currentSegmentTimestamp =
cache?.firstFrameTimestamp()?.let {
// in buffer mode we have to set the timestamp of the first frame as the actual start
DateUtils.getDateTime(it)
} ?: DateUtils.getDateTime(now - errorReplayDuration)
val duration = now - currentSegmentTimestamp.time
val replayId = currentReplayId
replayExecutor.submitSafely(options, "$TAG.$taskName") {
val segment =
createSegmentInternal(
duration,
currentSegmentTimestamp,
replayId,
currentSegment,
currentConfig.recordingHeight,
currentConfig.recordingWidth,
currentConfig.frameRate,
currentConfig.bitRate,
)
onSegmentCreated(segment)
}
}
}