<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Engineering at MoeGo]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>https://tech.moego.pet/</link><image><url>https://tech.moego.pet/favicon.png</url><title>Engineering at MoeGo</title><link>https://tech.moego.pet/</link></image><generator>Ghost 5.38</generator><lastBuildDate>Sun, 12 Apr 2026 11:44:44 GMT</lastBuildDate><atom:link href="https://tech.moego.pet/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[What Are Web Crawlers and How to Use Them Responsibly?]]></title><description><![CDATA[<h1 id="web-crawlers-introduction">Web Crawlers Introduction</h1><p>Let&apos;s start from the very beginning: what exactly are web crawlers? Simply put, web crawlers, also known as spiders or bots, are automated programs that systematically browse the internet, collecting data from websites. They play a crucial role in indexing information for search engines like</p>]]></description><link>https://tech.moego.pet/what-are-web-crawlers-and-how-to-use-them-responsibly/</link><guid isPermaLink="false">667298d59cd800004e919370</guid><dc:creator><![CDATA[Bob Bao]]></dc:creator><pubDate>Fri, 21 Jun 2024 10:25:09 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1446508431468-060dbf1f9633?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDh8fHdlYiUyMHNwaWRlcnxlbnwwfHx8fDE3MTg3ODk1MjF8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<h1 id="web-crawlers-introduction">Web Crawlers Introduction</h1><img src="https://images.unsplash.com/photo-1446508431468-060dbf1f9633?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDh8fHdlYiUyMHNwaWRlcnxlbnwwfHx8fDE3MTg3ODk1MjF8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="What Are Web Crawlers and How to Use Them Responsibly?"><p>Let&apos;s start from the very beginning: what exactly are web crawlers? Simply put, web crawlers, also known as spiders or bots, are automated programs that systematically browse the internet, collecting data from websites. They play a crucial role in indexing information for search engines like Google, enabling users to find relevant information quickly. However, their application extends far beyond search engines, finding use in data analysis, market research, and even in personal projects. Despite their utility, the operation of web crawlers comes with significant responsibilities and limitations, governed by both legal and ethical standards.</p><h1 id="legal-boundaries">Legal Boundaries</h1><p>The internet, often perceived as a vast and open space, is not exempt from legal regulations. Laws governing the use of web crawlers are akin to school rules for students. For instance, during exams, teachers enforce strict no-cheating policies. Similarly, laws prohibit unauthorized access to websites. In the United States, unauthorized data scraping can violate the Computer Fraud and Abuse Act (CFAA), leading to serious legal repercussions. In China, the legal landscape is somewhat more ambiguous, but the primary principles remain:</p><ol><li>Do not access data without explicit permission.</li><li>Do not disrupt the normal operation of websites.</li></ol><p>A landmark case highlighting these principles occurred in April 2019, when the Shenzhen Intermediate People&apos;s Court handled <a href="https://wb.sznews.com/MB/content/201904/26/content_642651.html?ref=engineering-at-moego">China&apos;s first &quot;crawler&quot; software lawsuit</a>. The defendant, the developer of the app &quot;Chelaile,&quot; was found guilty of using crawler technology to extract large amounts of data from a competitor, resulting in a court order to pay 500,000 RMB in damages.</p><h1 id="ethical-boundaries">Ethical Boundaries</h1><p>While legal boundaries set the minimum standards for behavior, ethical considerations often serve as our moral compass. Imagine you&apos;re at a buffet: while it&apos;s permissible to take food, it&apos;s unethical to hoard more than you can eat, resulting in waste. Similarly, web crawlers can collect data, but they must do so responsibly. Overloading a website with requests can cause disruptions, like causing chaos at the buffet.</p><p>Privacy is another critical ethical consideration. Collecting personal information without consent is like peeping through someone&apos;s window with binoculars &#x2013; it&apos;s invasive and illegal. Therefore, ensure that the data your crawler collects is public, and legal, and doesn&apos;t infringe on user privacy.</p><h1 id="responsible-use-of-web-crawlers">Responsible Use of Web Crawlers</h1><p>To use web crawlers responsibly within the confines of law and ethics, consider the following best practices:</p><ol><li><strong>Respect Website Policies</strong>: Always read the <code>robots.txt</code> file of websites. This file acts as a guide, specifying which parts of the site can be accessed by crawlers and which parts are off-limits.</li><li><strong>Limit Access Frequency</strong>: Avoid overloading websites by incorporating appropriate intervals between requests, mimicking the behavior of a human user.</li><li><strong>Use Proxies and RPA Software</strong>: These tools can help distribute requests, reduce the risk of being blocked, and simulate normal user behavior.</li><li><strong>Opt for Legal Data Acquisition</strong>: Whenever possible, acquire data through legitimate means. Purchasing data through proper channels is often safer and more reliable than risking legal consequences by scraping it.</li></ol><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://tech.moego.pet/content/images/2024/06/image.png" class="kg-image" alt="What Are Web Crawlers and How to Use Them Responsibly?" loading="lazy" width="1726" height="926" srcset="https://tech.moego.pet/content/images/size/w600/2024/06/image.png 600w, https://tech.moego.pet/content/images/size/w1000/2024/06/image.png 1000w, https://tech.moego.pet/content/images/size/w1600/2024/06/image.png 1600w, https://tech.moego.pet/content/images/2024/06/image.png 1726w" sizes="(min-width: 720px) 720px"><figcaption><a href="https://www.moego.pet/robots.txt?ref=engineering-at-moego">robots.txt on MoeGo</a></figcaption></figure><h1 id="defending-against-crawlers">Defending Against Crawlers</h1><p>The relationship between crawlers and anti-crawling measures is a dynamic, cat-and-mouse game, much like the interplay between swords and shields. For engineers, defending against unauthorized crawlers is crucial. Here are some effective strategies:</p><ol><li><strong>Request Limiting and Denial</strong>: Implement rate limiting and access control measures to restrict the number of requests from a single source.</li><li><strong>Client Authentication</strong>: Utilize methods like IP blocking and user-agent filtering to identify and block suspicious activity.</li><li><strong>Text Obfuscation and Dynamic Rendering</strong>: Mix text with images and use CSS to obfuscate data, making it difficult for crawlers to parse.</li><li><strong>Captchas</strong>: From basic text challenges to sophisticated puzzles, captchas are a well-known defense mechanism.</li></ol><p>For example, <a href="https://flight.qunar.com/site/oneway_list.htm?searchDepartureAirport=%E5%8C%97%E4%BA%AC&amp;3BsearchArrivalAirport=%E4%B8%8A%E6%B5%B7&amp;3BsearchDepartureTime=2024-06-20&amp;3BsearchArrivalTime=2024-06-22&amp;3BnextNDays=0&amp;3BstartSearch=true&amp;3BfromCode=BJS&amp;3BtoCode=SHA&amp;3Bfrom=flight_dom_search&amp;3BlowestPrice=null&amp;ref=engineering-at-moego">Qunar.com</a> employs CSS shifts to display ticket prices, complicating data extraction. Captchas have evolved from simple text-based challenges to complex spatial reasoning puzzles that even humans find difficult, illustrating the intensifying battle against crawlers.</p><h1 id="how-moego-defending-against-crawlers">How <a href="https://www.moego.pet/?ref=engineering-at-moego">MoeGo</a> Defending Against Crawlers</h1><p>To address the issue of malicious data scraping and unauthorized submissions, we have implemented Google reCAPTCHA on each user&apos;s <a href="https://www.moego.pet/online-booking?ref=engineering-at-moego">MoeGo Online Booking</a> website. This powerful tool helps us distinguish between human users and automated bots, thereby enhancing the security and integrity of the booking process. By deploying reCAPTCHA, we ensure that the data entered and accessed on our platforms is protected against unwanted intrusions, providing our users with a safer and more reliable online experience.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://tech.moego.pet/content/images/2024/06/image-2.png" class="kg-image" alt="What Are Web Crawlers and How to Use Them Responsibly?" loading="lazy" width="1080" height="2084" srcset="https://tech.moego.pet/content/images/size/w600/2024/06/image-2.png 600w, https://tech.moego.pet/content/images/size/w1000/2024/06/image-2.png 1000w, https://tech.moego.pet/content/images/2024/06/image-2.png 1080w" sizes="(min-width: 720px) 720px"><figcaption>MoeGo Online Booking with reCAPTCHA Verification</figcaption></figure><h1 id="summary">Summary</h1><p>Web crawlers are a double-edged sword. When used correctly, they can provide valuable insights and streamline information gathering. However, when misused, they can lead to legal troubles and ethical dilemmas. As engineers at MoeGo, it is imperative to master the technology while adhering to legal and ethical standards. By fostering a strong sense of security and responsibility in our development practices, we can become conscientious digital citizens, leveraging the power of web crawlers to enhance our work without crossing boundaries.</p><h1 id="author-bio">Author Bio</h1><p>Bob (bob@moego.pet), from MoeGo R&amp;D.</p><h1 id="references">References</h1><ol><li><a href="https://en.wikipedia.org/wiki/Web_crawler?ref=engineering-at-moego">Web crawel definition</a></li><li><a href="https://www.cloudflare.com/learning/bots/what-is-a-web-crawler/?ref=engineering-at-moego">What is a web crawler? | How web spiders work</a></li><li><a href="https://sitebulb.com/resources/guides/how-to-crawl-responsibly-the-need-for-less-speed/?ref=engineering-at-moego">How to Crawl Responsibly: The Need for (Less) Speed</a></li><li><a href="https://en.wikipedia.org/wiki/Robots.txt?ref=engineering-at-moego">Robots.txt definition</a></li><li><a href="https://prezi.com/view/h7EkIqW8DmCWZTb8kd1C/?ref=engineering-at-moego">Law and Ethics: The Boundaries of Web Crawlers</a></li></ol><hr><p>This work is licensed under a <a href="https://creativecommons.org/licenses/by-nc-nd/4.0/?ref=engineering-at-moego"><u>Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License </u></a>.</p>]]></content:encoded></item><item><title><![CDATA[Handle dependency version conflict on Android]]></title><description><![CDATA[<p>MoeGo mobile app offers in-person payment ability, powered by Stripe and Square, to our groommers. We integrate both the Stripe Terminal SDK and the Square Reader SDK, and they share some common stuffs which sometimes bring us problem.</p><p>When working on our latest release, we upgrades the Stripe Terminal React</p>]]></description><link>https://tech.moego.pet/handle-dependency-version-conflict-on-android/</link><guid isPermaLink="false">6455c963310ad1007be23cec</guid><dc:creator><![CDATA[Perqin]]></dc:creator><pubDate>Thu, 15 Jun 2023 02:45:06 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1509587837663-52b8687980c5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI5fHxlbGVwaGFudHxlbnwwfHx8fDE2ODY3Mjk4OTN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1509587837663-52b8687980c5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI5fHxlbGVwaGFudHxlbnwwfHx8fDE2ODY3Mjk4OTN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Handle dependency version conflict on Android"><p>MoeGo mobile app offers in-person payment ability, powered by Stripe and Square, to our groommers. We integrate both the Stripe Terminal SDK and the Square Reader SDK, and they share some common stuffs which sometimes bring us problem.</p><p>When working on our latest release, we upgrades the Stripe Terminal React Native SDK to the latest version to support Tap to Pay on iPhone, where the native Stripe Terminal SDKs on both Android and iOS are upgraded. This works fine on iOS, but the app starts crashing on Android when connecting to Sqaure reader!</p><h1 id="inspect-dependencies">Inspect dependencies</h1><p>Let&apos;s see the crash log first:</p><pre><code>2023-04-26 13:15:07.059 23949-23949/? E/AndroidRuntime: FATAL EXCEPTION: main
 &#xA0;  Process: com.moement.moego.business, PID: 23949
 &#xA0;  java.lang.NoSuchMethodError: No static method createRuntimeMessageAdapter(Ljava/lang/Class;)Lcom/squareup/wire/internal/RuntimeMessageAdapter; in class Lcom/squareup/wire/internal/ReflectionKt; or its super classes (declaration of &apos;com.squareup.wire.internal.ReflectionKt&apos; appears in /data/app/~~sA_8QsQq_POvPmiiiyky5Q==/com.moement.moego.business-egP-SXVX5YwQGid8AXqG7w==/base.apk!classes12.dex)
 &#xA0; &#xA0; &#xA0;  at com.squareup.wire.WireTypeAdapterFactory.create(WireTypeAdapterFactory.kt:77)
 &#xA0; &#xA0; &#xA0;  at com.google.gson.Gson.getAdapter(Gson.java:556)
 &#xA0; &#xA0; &#xA0;  at retrofit2.converter.gson.GsonConverterFactory.requestBodyConverter(GsonConverterFactory.java:74)
 &#xA0; &#xA0; &#xA0;  at retrofit2.Retrofit.nextRequestBodyConverter(Retrofit.java:315)
 &#xA0; &#xA0; &#xA0;  at retrofit2.Retrofit.requestBodyConverter(Retrofit.java:293)
 &#xA0; &#xA0; &#xA0;  at retrofit2.RequestFactory$Builder.parseParameterAnnotation(RequestFactory.java:778)
 &#xA0; &#xA0; &#xA0;  at retrofit2.RequestFactory$Builder.parseParameter(RequestFactory.java:325)
 &#xA0; &#xA0; &#xA0;  at retrofit2.RequestFactory$Builder.build(RequestFactory.java:206)
 &#xA0; &#xA0; &#xA0;  at retrofit2.RequestFactory.parseAnnotations(RequestFactory.java:67)
 &#xA0; &#xA0; &#xA0;  at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:26)
 &#xA0; &#xA0; &#xA0;  at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:202)
 &#xA0; &#xA0; &#xA0;  at retrofit2.Retrofit$1.invoke(Retrofit.java:160)
 &#xA0; &#xA0; &#xA0;  at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
 &#xA0; &#xA0; &#xA0;  at $Proxy7.login(Unknown Source)
 &#xA0; &#xA0; &#xA0;  at com.squareup.sdk.reader.authorization.ReaderSdkAuthenticator.login(ReaderSdkAuthenticator.kt:116)
 &#xA0; &#xA0; &#xA0;  at com.squareup.sdk.reader.authorization.RealAuthorizationManager.authorize(RealAuthorizationManager.java:109)
 &#xA0; &#xA0; &#xA0;  at com.squareup.sdk.reader.react.AuthorizationModule$2.run(AuthorizationModule.java:117)
 &#xA0; &#xA0; &#xA0;  at android.os.Handler.handleCallback(Handler.java:938)
 &#xA0; &#xA0; &#xA0;  at android.os.Handler.dispatchMessage(Handler.java:99)
 &#xA0; &#xA0; &#xA0;  at android.os.Looper.loop(Looper.java:246)
 &#xA0; &#xA0; &#xA0;  at android.app.ActivityThread.main(ActivityThread.java:8633)
 &#xA0; &#xA0; &#xA0;  at java.lang.reflect.Method.invoke(Native Method)
 &#xA0; &#xA0; &#xA0;  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
 &#xA0; &#xA0; &#xA0;  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)</code></pre><p>To find out what&apos;s wrong, we should open the Android project with the Android Studio. Double-hit the Shift key and search for <code>WireTypeAdapterFactory</code>, which is complaining <code>No static method createRuntimeMessageAdapter</code>:</p><figure class="kg-card kg-image-card"><img src="https://tech.moego.pet/content/images/2023/05/image-20230505114305327.png" class="kg-image" alt="Handle dependency version conflict on Android" loading="lazy" width="1848" height="1048" srcset="https://tech.moego.pet/content/images/size/w600/2023/05/image-20230505114305327.png 600w, https://tech.moego.pet/content/images/size/w1000/2023/05/image-20230505114305327.png 1000w, https://tech.moego.pet/content/images/size/w1600/2023/05/image-20230505114305327.png 1600w, https://tech.moego.pet/content/images/2023/05/image-20230505114305327.png 1848w" sizes="(min-width: 720px) 720px"></figure><p>The <code>WireTypeAdapterFactory.kt</code> coming from <code>wire-gson-support-3.7.0</code> is calling the <code>createRuntimeMessageAdapter(Class&lt;*&gt;)</code>, which is provided in <code>wire-runtime:3.7.0</code>.</p><figure class="kg-card kg-image-card"><img src="https://tech.moego.pet/content/images/2023/05/image-20230505120225560.png" class="kg-image" alt="Handle dependency version conflict on Android" loading="lazy" width="1944" height="1190" srcset="https://tech.moego.pet/content/images/size/w600/2023/05/image-20230505120225560.png 600w, https://tech.moego.pet/content/images/size/w1000/2023/05/image-20230505120225560.png 1000w, https://tech.moego.pet/content/images/size/w1600/2023/05/image-20230505120225560.png 1600w, https://tech.moego.pet/content/images/2023/05/image-20230505120225560.png 1944w" sizes="(min-width: 720px) 720px"></figure><p>But wait, if we switch to the Project view, we will find that there are 2 <code>wire-runtime</code> dependencies of different versions. In <code>wire-runtime-jvm:4.4.3</code>, the single-parameter version of <code>createRuntimeMessageAdapter</code> is replaced by a two-parameter one!</p><p>In Java world, every class is given a package name, and two classes with the same name inside the same package lead to the compile error (the famous duplicate class error). That means we can&apos;t have two different versions of the same dependency in our app, though it is common in Node.js projects.</p><p>To figure out which version is chosen, we open the terminal and run the following command under the <code>android</code> directory:</p><pre><code class="language-shell">./gradlew :app:dependencyInsight --configuration debugRuntimeClasspath --dependency wire-runtime</code></pre><p>This will execute a Gradle task to tell us the detail about dependencies matching <code>wire-runtime</code> pattern. We choose <code>debugRuntimeClasspath</code> instead of <code>debugCompileClasspath</code> to get the full dependency tree vision.</p><pre><code>com.squareup.wire:wire-runtime:4.4.3
+--- com.squareup.wire:wire-moshi-adapter:4.4.3
| &#xA0;  \--- com.stripe:stripeterminal-core:2.17.1
| &#xA0; &#xA0; &#xA0; &#xA0; \--- com.stripe:stripeterminal:2.17.1
| &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  +--- debugRuntimeClasspath (requested com.stripe:stripeterminal:2.8.0)
| &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  \--- project :moego_stripe-terminal-react-native
| &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; \--- debugRuntimeClasspath
\--- com.stripe:stripeterminal-core:2.17.1 (*)
&#x200B;
com.squareup.wire:wire-runtime:2.2.0 -&gt; 4.4.3
\--- com.squareup.retrofit2:converter-wire:2.7.2
 &#xA0; &#xA0; \--- com.squareup.sdk.reader:reader-sdk-xxxxxxxx:1.5.6
 &#xA0; &#xA0; &#xA0; &#xA0;  \--- com.squareup.sdk.reader:reader-sdk-xxxxxxxx:1.5.6
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; \--- project :react-native-square-reader-sdk
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  \--- debugRuntimeClasspath
&#x200B;
com.squareup.wire:wire-runtime:3.7.0 -&gt; 4.4.3
+--- com.squareup.sdk.reader:reader-sdk-xxxxxxxx:1.5.6
| &#xA0;  \--- com.squareup.sdk.reader:reader-sdk-xxxxxxxx:1.5.6
| &#xA0; &#xA0; &#xA0; &#xA0; \--- project :react-native-square-reader-sdk
| &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  \--- debugRuntimeClasspath
\--- com.squareup.wire:wire-gson-support:3.7.0
 &#xA0; &#xA0; \--- com.squareup.sdk.reader:reader-sdk-xxxxxxxx:1.5.6 (*)</code></pre><p>From the output we are told that <code>wire-gson-support:3.7.0</code> depends on <code>wire-runtime:3.7.0</code>, but the latter is pulled up to 4.4.3 which breaks our app.</p><h1 id="the-normal-approach-stick-to-the-previous-version">The normal approach: stick to the previous version</h1><p>In most cases, we could just force Gradle to use the specific version of some dependency. We checkout the app code to the revision where the Tap to Pay on iPhone is not introduced yet, and run the dependency insight task again to find the &quot;safe&quot; version of <code>wire-runtime</code>: 4.0.0.</p><p>To lock a dependency to some version, edit <code>android/app/build.gradle</code> and add the following line to the <code>dependencies</code> block:</p><pre><code class="language-groovy">dependencies {
 &#xA0;implementation (&apos;com.squareup.wire:wire-runtime:4.0.0&apos;) {
 &#xA0; &#xA0;version {
 &#xA0; &#xA0; &#xA0;strictly &apos;4.0.0&apos;
 &#xA0;  }
  }
 &#xA0;// Other code...
}</code></pre><p>Now the Square reader connection should work as expected, but if we are trying to discover the Stripe readers...</p><pre><code>Caused by: java.lang.NoSuchMethodError: No static method createRuntimeMessageAdapter(Ljava/lang/Class;Z)Lcom/squareup/wire/internal/RuntimeMessageAdapter; in class Lcom/squareup/wire/internal/ReflectionKt; or its super classes (declaration of &apos;com.squareup.wire.internal.ReflectionKt&apos; appears in /data/app/~~SjNakX7DciNr401J_dIdhA==/com.moement.moego.business-eVKkHNcBJoM42vrVJOJHng==/base.apk!classes30.dex)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.squareup.wire.WireJsonAdapterFactory.create(WireJsonAdapterFactory.kt:82)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.squareup.moshi.Moshi.adapter(Moshi.java:146)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.squareup.moshi.Moshi.adapter(Moshi.java:106)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.squareup.moshi.Moshi.adapter(Moshi.java:80)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.core.redaction.Extensions.toLogJson(Extensions.kt:20)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.core.redaction.Extensions.toLogJson$default(Extensions.kt:10)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.api.ApiLogPointInterceptor.intercept(ApiLogPointInterceptor.kt:26)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.core.restclient.InterceptorChain.proceed(InterceptorChain.kt:46)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.core.restclient.RestClient.execute(RestClient.kt:115)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.core.restclient.RestClient.blockingPost(RestClient.kt:57)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.proto.api.rest.MainlandApi.discoverLocations(MainlandApi.kt:336)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.proto.api.rest.MainlandApi.discoverLocations$default(MainlandApi.kt:327)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.core.transaction.AuthenticatedRestClient.discoverLocations(AuthenticatedRestClient.kt:233)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.api.ApiClient.discoverLocations(ApiClient.kt:162)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.resourcerepository.OnlineDirectResourceRepository.getReaderLocations(OnlineDirectResourceRepository.kt:832)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.resourcerepository.DirectResourceRepositoryRouter.getReaderLocations(DirectResourceRepositoryRouter.kt:173)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.resourcerepository.ProxyResourceRepository$getReaderLocations$1.invoke(ProxyResourceRepository.kt:126)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.resourcerepository.ProxyResourceRepository$getReaderLocations$1.invoke(ProxyResourceRepository.kt:126)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.resourcerepository.ProxyResourceRepository.withCurrentRepository(ProxyResourceRepository.kt:133)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.resourcerepository.ProxyResourceRepository.getReaderLocations(ProxyResourceRepository.kt:126)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.adapter.BbposAdapterLegacy.getLocationsForDiscovery(BbposAdapterLegacy.kt:805)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.adapter.BbposBluetoothAdapterLegacy.getLocationsForDiscovery(BbposBluetoothAdapterLegacy.kt:225)
15:43:24.244  E   at com.stripe.stripeterminal.internal.common.adapter.BbposBluetoothAdapterLegacy$DiscoverBluetoothReadersOperation.onUpdateDiscoveredReaders$lambda$6(BbposBluetoothAdapterLegacy.kt:577)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.adapter.BbposBluetoothAdapterLegacy$DiscoverBluetoothReadersOperation.lambda$OfuxcO_kCKP9oiD0iKlAxYfWlLs(Unknown Source:0)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.adapter.-$$Lambda$BbposBluetoothAdapterLegacy$DiscoverBluetoothReadersOperation$OfuxcO_kCKP9oiD0iKlAxYfWlLs.run(Unknown Source:6)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at io.reactivex.rxjava3.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:38)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  ... 5 more</code></pre><p>Ok, it turns out that Stripe Terminal SDK requires the latest version of <code>wire-runtime</code> to work properly!</p><p>Since the Tap to Pay on iPhone feature is only implemented by the iOS edition of the Stripe Terminal SDK, what about locking the version of Stripe Terminal Android SDK? In previous revision, our app depends on <code>com.stripe:stripeterminal:2.14.0</code> which works well with <code>wire-runtime:4.0.0</code>.</p><p>We also give it a shot. This time the app doesn&apos;t crash, but behavior not very well... Open the Logcat view of Android Studio we may find some errors like this one:</p><pre><code>16:33:30.975  I  Rejecting re-init on previously-failed class java.lang.Class&lt;com.stripeterminalreactnative.MappersKt$WhenMappings&gt;: java.lang.NoSuchFieldError: No static field WISEPAD_3S of type Lcom/stripe/stripeterminal/external/models/DeviceType; in class Lcom/stripe/stripeterminal/external/models/DeviceType; or its superclasses (declaration of &apos;com.stripe.stripeterminal.external.models.DeviceType&apos; appears in /data/app/~~knBXOkTcLhdocfPMV5Um2g==/com.moement.moego.business-yk53x66eCZZZdbYD2X1n7w==/base.apk!classes31.dex)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; (Throwable with no stack trace)
16:33:30.977  E  com.stripeterminalreactnative.MappersKt$WhenMappings
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; java.lang.NoClassDefFoundError: com.stripeterminalreactnative.MappersKt$WhenMappings
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.mapFromDeviceType(Mappers.kt:113)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$mapFromReader$1.invoke(Mappers.kt:82)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$mapFromReader$1.invoke(Mappers.kt:76)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.nativeMapOf(Mappers.kt:63)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.mapFromReader(Mappers.kt:76)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$mapFromReaders$1.invoke(Mappers.kt:74)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$mapFromReaders$1.invoke(Mappers.kt:74)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.collectToWritableArray(Mappers.kt:448)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.mapFromReaders(Mappers.kt:74)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.listener.RNDiscoveryListener$onUpdateDiscoveredReaders$1.invoke(RNDiscoveryListener.kt:29)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.listener.RNDiscoveryListener$onUpdateDiscoveredReaders$1.invoke(RNDiscoveryListener.kt:28)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.ReactExtensions$sendEvent$1$1.invoke(ReactExtensions.kt:16)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.ReactExtensions$sendEvent$1$1.invoke(ReactExtensions.kt:15)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.nativeMapOf(Mappers.kt:63)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.ReactExtensions.sendEvent(ReactExtensions.kt:15)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.listener.RNDiscoveryListener.onUpdateDiscoveredReaders(RNDiscoveryListener.kt:28)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.callable.ProxyDiscoveryListener.onUpdateDiscoveredReaders(ProxyDiscoveryListener.kt:17)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.adapter.RemoteReaderAdapter$DiscoverReadersOperation.execute(RemoteReaderAdapter.kt:153)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.adapter.RemoteReaderAdapter$DiscoverReadersOperation.execute(RemoteReaderAdapter.kt:147)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.adapter.RemoteReaderAdapter.discoverReaders(RemoteReaderAdapter.kt:128)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.adapter.ProxyAdapter.discoverReaders(ProxyAdapter.kt:230)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession$DiscoverReadersOperation.executeIfNotCanceled(TerminalSession.kt:1443)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession$CancelableOperation.execute(TerminalSession.kt:693)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession$ExternalOperation.run$core_publish(TerminalSession.kt:651)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession.enqueueOperation$lambda-2(TerminalSession.kt:513)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession.lambda$JON2uE3bqI6fqJ9Sw06Ciz8JRZw(Unknown Source:0)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.-$$Lambda$TerminalSession$JON2uE3bqI6fqJ9Sw06Ciz8JRZw.run(Unknown Source:4)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:463)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.util.concurrent.FutureTask.run(FutureTask.java:264)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.lang.Thread.run(Thread.java:1012)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; Caused by: java.lang.NoSuchFieldError: No static field WISEPAD_3S of type Lcom/stripe/stripeterminal/external/models/DeviceType; in class Lcom/stripe/stripeterminal/external/models/DeviceType; or its superclasses (declaration of &apos;com.stripe.stripeterminal.external.models.DeviceType&apos; appears in /data/app/~~knBXOkTcLhdocfPMV5Um2g==/com.moement.moego.business-yk53x66eCZZZdbYD2X1n7w==/base.apk!classes31.dex)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$WhenMappings.&lt;clinit&gt;(Unknown Source:135)
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.mapFromDeviceType(Mappers.kt:113)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$mapFromReader$1.invoke(Mappers.kt:82)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$mapFromReader$1.invoke(Mappers.kt:76)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.nativeMapOf(Mappers.kt:63)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.mapFromReader(Mappers.kt:76)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$mapFromReaders$1.invoke(Mappers.kt:74)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt$mapFromReaders$1.invoke(Mappers.kt:74)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.collectToWritableArray(Mappers.kt:448)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.mapFromReaders(Mappers.kt:74)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.listener.RNDiscoveryListener$onUpdateDiscoveredReaders$1.invoke(RNDiscoveryListener.kt:29)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.listener.RNDiscoveryListener$onUpdateDiscoveredReaders$1.invoke(RNDiscoveryListener.kt:28)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.ReactExtensions$sendEvent$1$1.invoke(ReactExtensions.kt:16)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.ReactExtensions$sendEvent$1$1.invoke(ReactExtensions.kt:15)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.MappersKt.nativeMapOf(Mappers.kt:63)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.ReactExtensions.sendEvent(ReactExtensions.kt:15)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripeterminalreactnative.listener.RNDiscoveryListener.onUpdateDiscoveredReaders(RNDiscoveryListener.kt:28)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.internal.common.callable.ProxyDiscoveryListener.onUpdateDiscoveredReaders(ProxyDiscoveryListener.kt:17)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.adapter.RemoteReaderAdapter$DiscoverReadersOperation.execute(RemoteReaderAdapter.kt:153)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.adapter.RemoteReaderAdapter$DiscoverReadersOperation.execute(RemoteReaderAdapter.kt:147)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.adapter.RemoteReaderAdapter.discoverReaders(RemoteReaderAdapter.kt:128)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.adapter.ProxyAdapter.discoverReaders(ProxyAdapter.kt:230)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession$DiscoverReadersOperation.executeIfNotCanceled(TerminalSession.kt:1443)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession$CancelableOperation.execute(TerminalSession.kt:693)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession$ExternalOperation.run$core_publish(TerminalSession.kt:651)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession.enqueueOperation$lambda-2(TerminalSession.kt:513)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.TerminalSession.lambda$JON2uE3bqI6fqJ9Sw06Ciz8JRZw(Unknown Source:0)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at com.stripe.stripeterminal.-$$Lambda$TerminalSession$JON2uE3bqI6fqJ9Sw06Ciz8JRZw.run(Unknown Source:4)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:463)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.util.concurrent.FutureTask.run(FutureTask.java:264)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)&#xA0;
 &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0; &#xA0;  at java.lang.Thread.run(Thread.java:1012)&#xA0;</code></pre><p>That&apos;s because the Android native part of the Stripe Terminal React Native SDK depends on the updated Stripe Terminal Android SDK (only some enums, though). To fix this issue, we have to fork the RN SDK and drop those usages.</p><p>Locking <code>com.stripe:stripeterminal</code>&apos;s version has another great downside: we will never receive the latest features and bugfixes from the Stripe official!</p><h2 id="the-hacky-approach-modify-wire-gson-support">The hacky approach: modify <code>wire-gson-support</code></h2><p>As we&apos;ve learnt above, <code>wire-gson-support:3.7.0</code>, required by Square Reader SDK, depends on <code>wire-runtime:3.7.0</code>. And fortunately, the <a href="https://github.com/square/wire?ref=engineering-at-moego"><u>wire</u></a> project is open source.</p><p>We clone the wire project and checkout the <code>3.7.0</code> tag.</p><p>On v4.4.3, here&apos;s the signature of the method:</p><pre><code class="language-kotlin">// wire-library/wire-runtime/src/jvmMain/kotlin/com/squareup/wire/internal/reflection.kt
fun &lt;M : Message&lt;M, B&gt;, B : Message.Builder&lt;M, B&gt;&gt; createRuntimeMessageAdapter(
 &#xA0;messageType: Class&lt;M&gt;,
 &#xA0;writeIdentityValues: Boolean,
): RuntimeMessageAdapter&lt;M, B&gt; {
 &#xA0;// ...
}</code></pre><p>There&apos;s only one added parameter. We copy the signature to the v3.7.0 code and update the usages. Then, run the Gradle task to build the jar file:</p><pre><code class="language-shell">../gradlew :wire-gson-support:assemble</code></pre><p>We will get the jar file under <code>wire-library/wire-gson-support/build/libs/wire-gson-support-3.7.0.jar</code>. The javadoc and sources jars can be safely ignored.</p><p>As the last step, we copy to jar file into our RN project&apos;s <code>android/app/libs/</code> directory and tell Gradle not to use the original <code>wire-gson-support</code> (or we will get duplicate classes error!). Modify <code>android/app/build.gradle</code> and add:</p><pre><code class="language-groovy">configurations {
 &#xA0;implementation {
 &#xA0; &#xA0;exclude group: &apos;com.squareup.wire&apos;, module: &apos;wire-gson-support&apos;
  }
}</code></pre><p>Now build and test the app!</p><h1 id="the-caveat">The caveat</h1><p>We have to remember in mind that the solution above is just a workaround. Our app may still be broken if the new version of <code>wire-runtime</code> introduces some breaking change and the old <code>wire-gson-support</code> can&apos;t play with it. We need to carefully review all the usages of <code>wire-runtime</code> by <code>wire-gson-support</code> to ensure that all incompatibilities are handled. And this approach is only possible when we can easily grab the source code of some dependency.</p>]]></content:encoded></item><item><title><![CDATA[Introduction to MySQL Slow Query Log]]></title><description><![CDATA[<p>Note: The following content uses MySQL version 8.0.24 and tables from the Sakila sample database.</p><h1 id="introduction">Introduction</h1><p>The Slow Query Log is a log used to record query statements that take a long time to execute in a MySQL database. In addition to recording DQL (Data Query Language), may</p>]]></description><link>https://tech.moego.pet/introduction-to-mysql-slow-query-log/</link><guid isPermaLink="false">64748a08ccd0b7004e387086</guid><dc:creator><![CDATA[Bob Bao]]></dc:creator><pubDate>Mon, 29 May 2023 11:28:52 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1597852074816-d933c7d2b988?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDR8fGRhdGFiYXNlfGVufDB8fHx8MTY4NTM1OTA2MHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1597852074816-d933c7d2b988?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDR8fGRhdGFiYXNlfGVufDB8fHx8MTY4NTM1OTA2MHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Introduction to MySQL Slow Query Log"><p>Note: The following content uses MySQL version 8.0.24 and tables from the Sakila sample database.</p><h1 id="introduction">Introduction</h1><p>The Slow Query Log is a log used to record query statements that take a long time to execute in a MySQL database. In addition to recording DQL (Data Query Language), may also record DDL (Data Definition Language).</p><p>Specifically, when the query time is longer than long_query_time and the number of rows checked exceeds min_examined_row_limit, the query statement will be recorded in the slow query log.</p><p>By analyzing the slow query log, you can identify query statements that take longer to execute and optimize them to improve the performance of MySQL database.</p><h1 id="configuration-introduction">Configuration Introduction</h1><h3 id="slowquerylog">slow_query_log</h3><p>Use slow_query_log to enable or disable slow query logging. The default is off.</p><p>Example usage:</p><pre><code class="language-SQL">SET GLOBAL slow_query_log = &apos;ON&apos;;</code></pre><h3 id="slowquerylogfile">slow_query_log_file</h3><p>Use the slow_query_log_file parameter to specify the output directory of the log. The default value is host_name-slow.log.</p><p>Example usage:</p><pre><code class="language-SQL">SET GLOBAL slow_query_log_file = &apos;/www/server/data/mysql-slow.log&apos;;</code></pre><h3 id="longquerytime">long_query_time</h3><p>the slow query threshold (in seconds) beyond which SQL execution will be logged. The default value is 10, the minimum value is 0, and the maximum value is 31536000 (365 days).</p><p>Example usage:</p><pre><code class="language-SQL">SET SESSION long_query_time = 1;</code></pre><h3 id="minexaminedrowlimit">min_examined_row_limit</h3><p>Slow query only logs SQL with scan rows greater than this parameter. only the number of rows checked at the server level is used here, not the number of rows scanned at the storage engine level. The default value is 0.</p><p>Example usage:</p><pre><code class="language-SQL">SET SESSION min_examined_row_limit = 100;</code></pre><h3 id="logoutput">log_output</h3><p>Use log_output to specify the log storage method. log_output=&apos;FILE&apos; means to store the log to a file, the default value is &apos;FILE&apos;. log_output=&apos;TABLE&apos; means to store the log to the database so that the log information is written to the mysql.slow_log table. The MySQL database supports two types of log storage at the same time, just configure them separated by commas, for example: log_output=&apos;FILE,TABLE&apos;. Logging to a dedicated system log table consumes more system resources than logging to a file, so for those who need to enable slow query logging and need to get higher system performance, it is recommended to prefer logging to a file.</p><p>Example usage:</p><pre><code class="language-SQL">SET GLOBAL log_output = &apos;FILE&apos;;</code></pre><h3 id="logslowadminstatements">log_slow_admin_statements</h3><p>log_slow_admin_statements can be used to log table operations, including ALTER TABLE, CREATE INDEX, and so on. The default is off.</p><p>Example usage:</p><pre><code class="language-SQL">SET GLOBAL log_slow_admin_statements = &apos;ON&apos;;</code></pre><h3 id="logqueriesnotusingindexes">log_queries_not_using_indexes</h3><p>log_queries_not_using_indexes records statements that do not use indexes. If the amount of data in the table is so small that a full table scan is faster and does not use the indexes, the same will be logged. The default value is off.</p><p>If only slow_query_log and log_queries_not_using_indexes are turned on, and the other parameters are the defaults, then the slow query log will log statements that use the UNION keyword, whether or not they hit an index. This appears to be a bug that is still not fixed in version 8.0.24: <a href="https://bugs.mysql.com/bug.php?id=29244&amp;ref=engineering-at-moego">https://bugs.mysql.com/bug.php?id=29244</a>.</p><p>Example usage:</p><pre><code class="language-SQL">SET GLOBAL log_queries_not_using_indexes = &apos;ON&apos;;</code></pre><h3 id="logthrottlequeriesnotusingindexes">log_throttle_queries_not_using_indexes</h3><p>log_throttle_queries_not_using_indexes to limit the number of statements that log no indexes for 60 seconds. The default is 0, which means no limit. Assuming a positive value of n, when the first statement without indexes is logged, MySQL opens a 60-second window where statements within the number n are logged and the rest are counted as a summary message.</p><p>Example usage:</p><pre><code class="language-SQL">SET GLOBAL log_throttle_queries_not_using_indexes = 200;</code></pre><p></p><p>The above is modified by dynamic configuration and restarting either MySQL or the server will cause the configuration to fail. This can be done by writing the parameters to the configuration file to make it permanent.</p><p>Example configuration:</p><pre><code>[mysqld]
slow_query_log=1
slow_query_log_file=/www/server/data/mysql-slow.log
long_query_time=1
min_examined_row_limit=100
log_queries_not_using_indexes=1</code></pre><h1 id="record-condition-judgment">Record condition judgment</h1><p>MySQL determines whether to log the current statement to the slow query log in the following order:</p><ol><li>If it is an administrative statement and log_slow_admin_statements is turned on, the subsequent judgment is made, otherwise it is not logged.</li><li>If the query time is greater than long_query_time or log_queries_not_using_indexes is turned on and no indexes are used, then follow up, otherwise no logging is done.</li><li>If the query statement satisfies the number of rows set by at least min_examined_row_limit, it will be followed up, otherwise it will not be logged.</li><li>If log_throttle_queries_not_using_indexes is set and the current statement meets the required number of rows, it is logged, otherwise it is not logged.</li></ol><p>With MySQL before 8.0, query statements that hit the cache are not logged to the slow query log.</p><h1 id="log-file-format">Log File Format</h1><p>Example of slow query log format:</p><pre><code># Time: 2023-05-29T09:09:28.842627Z
# User@Host: root[root] @ localhost []  Id:    12
# Query_time: 0.007624  Lock_time: 0.000363 Rows_sent: 1000  Rows_examined: 1006
SET timestamp=1685351368;
select *
from film f
         left join language l on f.language_id = l.language_id
where l.name = &apos;English&apos;;</code></pre><p>The first line: the specific time when the SQL query was executed.</p><p>The second line: the connection information, user and connection IP of the SQL query execution.</p><p>The third line: Query_time indicates the time of SQL execution, the longer it is, the slower it is, Lock_time indicates the time of waiting for table lock in MySQL server phase (not in storage engine phase), Rows_sent indicates the number of rows returned by the query, Rows_examined indicates the number of rows checked by the query, the longer it is, the more time consuming it is.</p><p>The fourth line: set the timestamp, which has no practical meaning, but only corresponds to the execution time with the first line.</p><p>The fifth row and all subsequent rows are the executed SQL statements, and the SQL may be very long. Until the next # Time.</p><h1 id="slow-query-log-analysis-tools">Slow Query Log Analysis Tools</h1><p>Use MySQL&apos;s own slow query logging tool, mysqldumpslow, to filter and summarize data.</p><p>Commonly used parameters are explained:</p><h3 id="s-sorttype">-s sort_type</h3><p>the type of sorting. The following options are available for sort_type: -s</p><ul><li>c: count Total number of executions</li><li>l: lock time</li><li>t: query time</li><li>r: number of records returned</li><li>al: average lock time</li><li>at&#xFF1A;Average query time</li><li>ar&#xFF1A;average number of returned records</li></ul><h3 id="t-n">-t N</h3><p>returns the first N rows of data.</p><h3 id="g-pattern">-g pattern</h3><p>filter out the required information, you can write regular expressions, similar to grep command.</p><p></p><p>Example: Query the top 10 statements sorted by time that contain a left join:</p><pre><code>mysqldumpslow -s t -t 10 -g &quot;left join&quot; /www/server/data/mysql-slow.log</code></pre><p>Query results:</p><pre><code>Reading mysql slow query log from /www/server/data/mysql-slow.log
Count: 1  Time=0.01s (0s)  Lock=0.00s (0s)  Rows=1000.0 (1000), root[root]@localhost
  select *
  from film f
  left join language l on f.language_id = l.language_id
  where l.name = &apos;S&apos;

Died at /usr/bin/mysqldumpslow line 162, &lt;&gt; chunk 2.</code></pre><h1 id="summary">Summary</h1><p>The slow query log is a log used to record query statements that take a long time to execute in a MySQL database. By analyzing the information in the slow query log, you can find out the query statements that take longer to execute and perform targeted optimization to improve the performance of MySQL database.</p><h1 id="author-bio">Author Bio</h1><p>Bob (bob@moego.pet), from MoeGo R&amp;D.</p><h1 id="references">References</h1><ol><li><a href="https://dev.mysql.com/doc/sakila/en/?ref=engineering-at-moego">https://dev.mysql.com/doc/sakila/en/</a></li><li><a href="https://dev.mysql.com/doc/refman/8.0/en/slow-query-log.html?ref=engineering-at-moego">https://dev.mysql.com/doc/refman/8.0/en/slow-query-log.html</a></li><li><a href="https://dev.mysql.com/doc/refman/8.0/en/mysqldumpslow.html?ref=engineering-at-moego">https://dev.mysql.com/doc/refman/8.0/en/mysqldumpslow.html</a></li><li><a href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html?ref=engineering-at-moego">https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html</a></li></ol><hr><p>This work is licensed under a <a href="https://creativecommons.org/licenses/by-nc-nd/4.0/?ref=engineering-at-moego"><u>Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License </u></a>.</p>]]></content:encoded></item></channel></rss>