Skip to content

Commit cf34f74

Browse files
[IBGCRASH-11420] Android symbols upload script (Crash Deobfuscation) (#220)
* Add gradle script to locate and zip obfuscation symbols files * Add upload symbols task in build.gradle * Add error handling to upload symbols script * Add http request logic to upload symbols script * Refactor upload symbols script * Add clean up to upload symbols script * Format upload symbols script * Remove null checks in upload symbols script Throw exceptions instead of repeated null checks * Split get_config in upload symbols script * Use function to combine paths in upload symbols script * Add a check for instabugUploadEnable flag * Add extra logs in upload symbols script * Add code-style changes for upload symbols script * Add support for gradle.properties in upload symbols script * Add constants to upload symbols script * Change variable definitions location priority Look for upload variables in local props -> gradle props -> env variable * Disable auto-upload by default * Add support for env. and localprops for enableFlag The script will look for the instabugUploadEnable in this order local->gradle->env * Move upload flag check to the script * Remove localProperties global variable
1 parent adf1e5d commit cf34f74

File tree

2 files changed

+265
-0
lines changed

2 files changed

+265
-0
lines changed

android/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,11 @@ dependencies {
3737
implementation 'com.instabug.library:instabug:10.11.1'
3838
testImplementation 'junit:junit:4.12'
3939
}
40+
41+
// add upload_symbols task
42+
apply from: './upload_symbols.gradle'
43+
tasks.whenTaskAdded { task ->
44+
if (task.name == 'assembleRelease') {
45+
task.finalizedBy upload_symbols_task
46+
}
47+
}

android/upload_symbols.gradle

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import java.util.zip.ZipOutputStream
2+
import java.util.zip.ZipEntry
3+
4+
// Custom Exception for undefined variable
5+
public class VariableNotFoundException extends Exception {
6+
public VariableNotFoundException(String errorMessage) {
7+
super(errorMessage);
8+
}
9+
}
10+
// Constants
11+
final INSTABUG_APP_TOKEN = 'INSTABUG_APP_TOKEN'
12+
final INSTABUG_API_KEY = 'INSTABUG_API_KEY'
13+
final UPLOAD_URL = 'https://api.instabug.com/api/web/public/flutter-symbol-files/android'
14+
final INSTABUG_AUTO_UPLOAD_ENABLE = 'INSTABUG_AUTO_UPLOAD_ENABLE'
15+
ext.boundary = 'abcd' + Long.toString(System.currentTimeMillis()) * 2 + 'dcba'
16+
ext.lineEnd = '\r\n'
17+
18+
/**
19+
* task to upload symbols to Instabug endpoint
20+
*/
21+
task upload_symbols_task {
22+
doLast {
23+
try {
24+
// If obfuscate flag not provided, exit the task
25+
if (project['dart-obfuscation'] != 'true') {
26+
println "Instabug: obfuscate flag not used — skipping debug symbols upload"
27+
return
28+
}
29+
// Load local properties
30+
final localProperties = load_local_properties()
31+
32+
// If auto upload is disabled, exit the task
33+
final uploadFlag = get_variable(INSTABUG_AUTO_UPLOAD_ENABLE, localProperties)
34+
println "$INSTABUG_AUTO_UPLOAD_ENABLE: $uploadFlag"
35+
if (!uploadFlag?.toBoolean()) {
36+
println "Instabug: skipping debug symbols upload"
37+
return
38+
}
39+
40+
// Get upload configs
41+
final applicationToken = get_variable(INSTABUG_APP_TOKEN, localProperties);
42+
final apiKey = get_variable(INSTABUG_API_KEY, localProperties);
43+
final symbolsPath = get_symbols_path()
44+
45+
// Prepare zip file with symbols
46+
final zipFile = prepare_zip_file(symbolsPath)
47+
48+
upload_symbols(zipFile, applicationToken, apiKey, UPLOAD_URL)
49+
} catch (VariableNotFoundException e) {
50+
println "Instabug: uploading symbols: ${e.message}"
51+
} catch (Exception e) {
52+
println "Instabug err: uploading symbols: ${e.message}"
53+
}
54+
}
55+
}
56+
/**
57+
* load local properties into a global variable localProperties
58+
*/
59+
def load_local_properties() {
60+
// Get local properties
61+
final properties = new Properties()
62+
final propertiesFile = project.rootProject.file('local.properties')
63+
if (propertiesFile.exists()) {
64+
properties.load(propertiesFile.newDataInputStream())
65+
}
66+
return properties
67+
}
68+
/**
69+
* get environment variable
70+
* @param name variable name
71+
* @param localProperties local properties object
72+
* @return variable environment variable for the given name
73+
*/
74+
String get_variable(name, localProperties) {
75+
// Look for the variable in local properties, gradle properties and env variables
76+
final variable = localProperties.getProperty(name) ?: project.findProperty(name) ?: System.getenv(name)
77+
if (!variable) {
78+
throw new VariableNotFoundException("$name not found. Make sure you've added the environment variable $name")
79+
}
80+
println "Instabug: ${name} found"
81+
return variable;
82+
}
83+
84+
/**
85+
* get symbols path
86+
* @return symbolsPath the path of the symbols directory
87+
*/
88+
String get_symbols_path() {
89+
// Get root host directory path
90+
final rootFile = new File(project['rootDir'].toString())
91+
92+
// Get the path of the symbols directory
93+
final obfuscationPath = project['split-debug-info']
94+
final obfuscationFile = new File(obfuscationPath)
95+
96+
// If the provided path is relative
97+
// append the symbols directory to the root project path to get the full path
98+
if (obfuscationFile.absolute) {
99+
return obfuscationPath
100+
} else {
101+
return new File(rootFile.parent, obfuscationPath)
102+
}
103+
}
104+
105+
/**
106+
* search for the symbols files in the symbols directory
107+
* and create a zip file with the symbols files
108+
* @param symbolsPath the path of the symbols directory
109+
* @return zipFile the zip file created
110+
*/
111+
112+
File prepare_zip_file(symbolsPath) {
113+
// Check if the symbols directory provided exists
114+
final symbolsDir = new File(symbolsPath)
115+
if (!symbolsDir.exists()) {
116+
throw new Exception('Symbols directory not found')
117+
}
118+
119+
// Search in symbolsPath for files with extension .symbols and add them to a list
120+
final symbolsFiles = symbolsDir.listFiles().findAll { file -> file.path.endsWith('.symbols') }
121+
if (symbolsFiles.empty) {
122+
throw new Exception('No symbols files found')
123+
}
124+
println "Instabug: ${symbolsFiles.size()} Symbols files found:"
125+
126+
// Print list of symbols files
127+
symbolsFiles.forEach { file ->
128+
println "${file.path}"
129+
}
130+
131+
// Create a zip file with the symbols files
132+
final zipFile = zip_files(symbolsPath, symbolsFiles)
133+
134+
return zipFile
135+
}
136+
/**
137+
* create a zip file with the symbols files
138+
*
139+
* @param symbolsPath the path of the symbols directory
140+
* @param symbolsFiles the list of symbols files
141+
* @return zipFile the zip file created
142+
*/
143+
144+
File zip_files(symbolsPath, symbolsFiles) {
145+
final zipFile = new File(symbolsPath + '.zip')
146+
final zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile))
147+
symbolsFiles.forEach { file ->
148+
final fileName = file.path.substring(symbolsPath.length() + 1)
149+
final zipEntry = new ZipEntry(fileName)
150+
zipOutputStream.putNextEntry(zipEntry)
151+
FileInputStream fileInputStream = new FileInputStream(file)
152+
final buffer = new byte[1024]
153+
int length = fileInputStream.read(buffer)
154+
while (length > 0) {
155+
zipOutputStream.write(buffer, 0, length)
156+
length = fileInputStream.read(buffer)
157+
}
158+
fileInputStream.close()
159+
zipOutputStream.closeEntry()
160+
}
161+
zipOutputStream.close()
162+
println "Instabug: Zip file created at ${zipFile.path}"
163+
return zipFile
164+
}
165+
/**
166+
* upload symbol files to Instabug endpoint
167+
*
168+
* @param zipFile the zip file containing the symbols files
169+
* @param applicationToken the app token
170+
* @param apiKey the api key
171+
* @return void
172+
*/
173+
void upload_symbols(zipFile, applicationToken, apiKey, uploadUrl) {
174+
// Initialize connection
175+
final connection = new URL(uploadUrl).openConnection() as HttpURLConnection
176+
177+
// Set connection headers and request method
178+
connection.setRequestMethod('POST')
179+
connection.setRequestProperty('Connection', 'Keep-Alive')
180+
connection.setRequestProperty('Content-Type', "multipart/form-data;boundary=${boundary}")
181+
connection.setRequestProperty('Accept-Encoding', 'gzip')
182+
connection.setRequestProperty('Content-Length', "${zipFile.length()}")
183+
connection.setDoOutput(true)
184+
185+
// Initialize output stream and writer
186+
final outputStream = connection.getOutputStream()
187+
final charset = 'ISO-8859-1'
188+
final writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true)
189+
190+
// Write application_token in the request body
191+
writeField(writer, 'application_token', applicationToken)
192+
193+
// Write api_key in the request body
194+
writeField(writer, 'api_key', apiKey)
195+
196+
// Write zip file in the request body
197+
final fileName = zipFile.getName()
198+
writeFile(writer, fileName, zipFile, outputStream)
199+
200+
// Send the request
201+
writer.close()
202+
203+
// Get the response
204+
final responseCode = connection.getResponseCode()
205+
println "Instabug: Response Code: ${responseCode}"
206+
println "Instabug: Response Message: ${connection.getResponseMessage()}"
207+
if (responseCode == 200) {
208+
println 'Instabug: Upload successful'
209+
} else {
210+
println 'Instabug: Upload failed'
211+
}
212+
213+
// Close the connection
214+
connection.disconnect()
215+
// Delete the zip file
216+
zipFile.delete()
217+
}
218+
/**
219+
* write a field in the request body
220+
* @param writer the writer
221+
* @param field the name of the field
222+
* @param value the value of the field
223+
* @return void
224+
*/
225+
void writeField(writer, field, value) {
226+
writer.append('--' + boundary).append(lineEnd)
227+
writer.append("Content-Disposition: form-data; name=\"" + field + "\"").append(lineEnd)
228+
writer.append(lineEnd)
229+
writer.append(value).append(lineEnd)
230+
}
231+
/**
232+
* write a field in the request body
233+
* @param writer the writer
234+
* @param fileName the name of the file
235+
* @param file the file to be uploaded
236+
* @param outputStream the output stream
237+
* @return void
238+
*/
239+
void writeFile(writer, fileName, file, outputStream) {
240+
writer.append('--' + boundary).append(lineEnd)
241+
writer.append("Content-Disposition: form-data; name=\"" + 'file' + "\"; filename=\"" + fileName + "\"").append(lineEnd);
242+
writer.append('Content-Type: ' + URLConnection.guessContentTypeFromName(fileName)).append(lineEnd)
243+
writer.append(lineEnd)
244+
writer.flush()
245+
246+
final inputStream = new FileInputStream(file)
247+
byte[] buffer = new byte[4096]
248+
int bytesRead = -1
249+
while ((bytesRead = inputStream.read(buffer)) != -1) {
250+
outputStream.write(buffer, 0, bytesRead)
251+
}
252+
outputStream.flush()
253+
inputStream.close()
254+
255+
writer.append('--' + boundary).append(lineEnd)
256+
writer.flush()
257+
}

0 commit comments

Comments
 (0)