<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[ReviewTrackers Engineering, Product and Design Blog - Medium]]></title>
        <description><![CDATA[We are a team of PMs, Designers and Engineers solving the problem of online reputation management. We are using React, Ruby, Go and have an NLP service (in Python). RT is always hiring smart people to help solve our interesting problems - https://reviewtrackers.com/jobs - Medium]]></description>
        <link>https://medium.com/reviewtrackers-engineering?source=rss----62b16e39f4f---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>ReviewTrackers Engineering, Product and Design Blog - Medium</title>
            <link>https://medium.com/reviewtrackers-engineering?source=rss----62b16e39f4f---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 00:13:19 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/reviewtrackers-engineering" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Keeping Design Consistent]]></title>
            <link>https://medium.com/reviewtrackers-engineering/keeping-design-consistent-382364537c8a?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/382364537c8a</guid>
            <category><![CDATA[design]]></category>
            <category><![CDATA[product-design]]></category>
            <category><![CDATA[user-experience]]></category>
            <category><![CDATA[ux-design]]></category>
            <category><![CDATA[ux]]></category>
            <dc:creator><![CDATA[Allison Winter]]></dc:creator>
            <pubDate>Fri, 13 Jan 2023 14:04:12 GMT</pubDate>
            <atom:updated>2023-01-13T14:04:12.606Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>How to Keep Design Consistent</strong></h3><h4>A Year in Review for Design at ReviewTrackers</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iWveJ2vkHlq4aAnex_BEcg.png" /></figure><p>ReviewTrackers grew significantly this year, and as a result, the product and the design team grew with it. This indicated that we as a company were succeeding, but with this success we, as designers, noticed that this growth facilitated inconsistency across our designs. The product had grown, making it harder to manage, and the design team had grown, resulting in more personalities and perspectives.</p><p>To combat this looming inconsistency, the team implemented several efforts to ensure consistency was maintained now and as we continue to scale. In this article, we look to explore three specific ways our design team adjusted to growth.</p><h4>Implementing Design Principles</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uM3RicA50svAiD8HTw37DA.png" /></figure><p>As our design team grew, so did the differences in how we designed and thought. These very differences in our design processes are critical — nay I say essential — to creating a great product. However, we began to realize that our team lacked direction and continuity in how we designed.</p><p>What we needed was a tool that would provide guidance when making design decisions; a tool that reduces individual bias and realigns our mission as ReviewTrackers designers.</p><p>Ultimately, we decided to create a set of guiding design principles. These principles were intended to hold us accountable for delivering solutions for our users.</p><p>These design principles were implemented in July 2022. You can read more about their creation and implementation below.</p><p><a href="https://engineering.reviewtrackers.com/getting-principled-at-reviewtrackers-bafe9b3c12f6">Getting Principled at ReviewTrackers</a></p><p>As we reflect on this exercise, it becomes obvious to us that creating a set of user-focused design principles is something all scaling design teams should explore. These principles have allowed our team to focus on what’s important in our design approach.</p><h4>Design System Simplification</h4><p>In July 2021, the team designed and built<a href="https://engineering.reviewtrackers.com/establishing-a-design-system-at-reviewtrackers-4dc683270d59"> ReviewTracker’s design system,</a> which unified the look and feel of our product. After one year of implementation, we realized that there was an abundance of type styles and spacing options to choose from.</p><p>The problem was that we had too many <em>similar</em> options. It was nearly impossible to justify the usage of one over another. Why should someone choose medium font weight over regular? Why use 16px spacing instead of 24? Ultimately, the differences between these options were minimal, and we as designers were wasting time, and creating divergences trying to decide between them.</p><p>The design system was intended to bring consistency to our product. Yet, with so many choices and a growing team, it was becoming harder to achieve consistent designs. We needed to take a step back, look at our product as a whole, and prioritize consistency over visual perfection.</p><p>When we sat down to examine these two core elements of our design system we uncovered that we had 45 different type styles and 10 different spacing options.</p><p>After some examination, we cut the type styles down from 45 to 10, leaving 3 for headers, 4 for body, 1 for buttons, 1 for links, and 1 for pre-titles. These remaining 10 still allowed us to utilize type as a design element aiding visual design and hierarchy, but eliminated splitting hairs between similar options.</p><p>Turning our attention to spacing, we had previously used a linear spacing scale that increased by 8.</p><p>This gave us a total of 10 options for our spacing. We determined that by using a geometric spacing scale, we would not only reduce the number of options but also increase the contrast between choices.</p><p>Reducing the number of choices in these two instances allowed us to ensure that there are fewer choices made based on personal bias and less time spent making choices surrounding type and spacing. The resulting consistency manifests itself to the user as reduced noise, resulting in a more focused user experience.</p><h4>Redesigning our Navigation</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VNtyU0a2x6904gVXcGEWsQ.png" /></figure><p>Originally, ReviewTrackers utilized a horizontal-style navigation menu. However, as the product grew and features were added, the menu became overly complicated. This complexity began to impede how visitors used and understood the application.</p><p>It became clear that we needed to rethink our navigation menu to improve our users’ experience. So in September 2022, the team undertook an effort to<a href="https://medium.com/r?url=https%3A%2F%2Fengineering.reviewtrackers.com%2Frethinking-navigation-at-reviewtrackers-483b70345bbf"> redesign ReviewTracker’s navigational menu</a>. Essentially, we reorganized the application’s pages and features to allow for a more intuitive user experience.</p><p>Efforts like these can sometimes feel like they pause progress. However, as a product grows, design teams should consider how their application’s information architecture (IA) supports this product growth.</p><p>It’s a basic concept that new features add complexity to an application’s IA, and yet, it can be difficult for teams to recognize the need to mitigate this complexity, especially when it means pausing work on new features and functionality. It is our team’s ability to “pause progress” and prioritize an intuitive IA that will ultimately lead to greater feature adoption and a better user experience.</p><h4>Parting thoughts</h4><p>As we conclude this reflection, we hope you’ve enjoyed learning a bit about our team and how we design.</p><p>We’re always interested in growing our team and hearing from talented designers. If you have any questions or would like to get in touch, please reach out to product@reviewtrackers.com.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=382364537c8a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/keeping-design-consistent-382364537c8a">Keeping Design Consistent</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rethinking Navigation at ReviewTrackers]]></title>
            <link>https://medium.com/reviewtrackers-engineering/rethinking-navigation-at-reviewtrackers-483b70345bbf?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/483b70345bbf</guid>
            <category><![CDATA[ux-research]]></category>
            <category><![CDATA[product]]></category>
            <category><![CDATA[product-design]]></category>
            <category><![CDATA[ux-design]]></category>
            <category><![CDATA[ux]]></category>
            <dc:creator><![CDATA[John Orzechowski]]></dc:creator>
            <pubDate>Mon, 03 Oct 2022 13:01:21 GMT</pubDate>
            <atom:updated>2022-10-03T13:01:21.648Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="cover image — old nav (left) vs new nav (right)" src="https://cdn-images-1.medium.com/max/1024/1*mWP_0InOgv0c_6Bila7Trw.png" /></figure><h4>Co-written by <a href="https://www.linkedin.com/in/johnorzechowskijr">John Orzechowski</a>, <a href="https://www.linkedin.com/in/reneepatel">Renee Patel</a>, <a href="https://www.linkedin.com/in/jacobcarterdesign">Jake Carter</a>, and <a href="https://www.linkedin.com/in/cecilia-xie">Cici Xie</a></h4><p>As a design team, we’re always looking for ways to improve our processes and product. Some examples of this are <a href="https://engineering.reviewtrackers.com/establishing-a-design-system-at-reviewtrackers-4dc683270d59">Establishing a Design System</a> and <a href="https://engineering.reviewtrackers.com/getting-principled-at-reviewtrackers-bafe9b3c12f6">Getting Principled</a>. It’s no surprise that our ambition outgrew our current strategy: horizontal top-bar navigation.</p><p>How do we channel our ambition for continuous improvement? By completing a redesign of our product’s navigation.</p><h3><strong>Why Redesign</strong></h3><p>Our data shows that ReviewTrackers is used on larger viewports. More specifically, 85% of users use desktop and 15% of users use mobile. Users usually have ample horizontal space. But, our product matured to a point where that horizontal space was no longer enough.</p><p>As a workaround, we had to wrap the labels of some menu items. It felt clunky, outdated, and suppressive to innovation. It became clear that we needed to rethink our strategy for the sake of our company’s continued growth, and to future proof the navigation for our long-term plans. We couldn’t settle for a band-aid solution, we needed to approach this problem as a clean slate.</p><p>With guidance from our VP of Product, we aimed for an opt-out rate of less than 25%. If we stayed under that mark, then the redesign is a success.</p><p>What steps did we take to make this redesign happen and what was our release strategy?</p><h3><strong>Redesign Process</strong></h3><p>Our redesign process took place in sequential steps.</p><h4><strong>1. Research</strong></h4><p><strong>Comparative analysis<br></strong>First, we conducted a comparative analysis. This allowed us to see what other companies were doing well and what we’d like to do differently.</p><figure><img alt="B2B products audited in the comparative analysis" src="https://cdn-images-1.medium.com/max/1024/1*OirL_gwij1BQGbZi3WOj2A.png" /></figure><p><strong>Research proposal</strong><br>Next, we created a research proposal. This provided an overview on our research approach, which led us into the card sort phase.</p><figure><img alt="google doc containing one page of a research proposal" src="https://cdn-images-1.medium.com/max/1024/1*w90PcD4JhwmbQ_l_BxB0CQ.png" /></figure><p><strong>Card sorts<br></strong>We conducted two types of card sorts: open and hybrid.</p><p>The open card sort had 45 participants and about 14% margin of error. The hybrid card sort had 23 participants and about 20% margin of error. The card sorts provided us with behavioral insight on groupings and categories.</p><p>To determine the final structure, we referenced user input, ‘do’ vs. ‘set up’, and used design judgment.</p><figure><img alt="card sort data groupings and similarity matrix" src="https://cdn-images-1.medium.com/max/1024/1*yavwTIe8vBHBGJ218f0nXw.png" /></figure><h4><strong>2. Prototype and Test</strong></h4><p><strong>Design</strong><br>After conducting research, we worked on designing the new navigation. This took many iterations, which led to our final design.</p><figure><img alt="many art boards in Figma" src="https://cdn-images-1.medium.com/max/1024/1*rcyGgVzz2Uhasmqp9aQUKg.png" /></figure><p><strong>Build prototype</strong><br>We took our final design and built a prototype for usability testing.</p><figure><img alt="new navigation prototype" src="https://cdn-images-1.medium.com/max/1024/1*coMO2iy1pxfG6v6GEiYbCA.png" /></figure><p><strong>Usability test </strong><br>We tested the prototype with 12 users and used a spreadsheet for thematic analysis.</p><figure><img alt="usability test findings organized in a spreadsheet" src="https://cdn-images-1.medium.com/max/1024/1*rHohpT-KE3OIB2hyVEXQwQ.png" /></figure><p><strong>Collaborate with Engineering team </strong><br>We added a page in Figma to communicate the final design with the engineering team.</p><figure><img alt="several art boards in Figma" src="https://cdn-images-1.medium.com/max/1024/1*n1prsTQeaGxEFtIiDuPvaA.png" /></figure><p><strong>Release and iterate with Customer Success (CS) team<br></strong>Once developed, we released the navigation to CS and iterated based on their feedback.</p><p><strong>Quality Assurance (QA) testing</strong><br>Before entering a beta stage, QA Engineers tested the new navigation.</p><figure><img alt="the new navigation ticket in Pivotal Tracker" src="https://cdn-images-1.medium.com/max/1024/1*VYganceJOTNaYatlNz0KgA.png" /></figure><h4><strong>3. Release and Monitor</strong></h4><p><strong>Beta — Release and Iterate<br></strong>On June 3, 2022, we released the new navigation to 209 accounts. Within those accounts, about 700 users viewed the new navigation. We had an opt-out rate of 4.6%. From our experience with the beta, we made many improvements.</p><p><strong>General Availability (GA) — Release and Iterate<br></strong>On July 6, 2022, we released the new navigation to all remaining accounts (807)*. At the time of writing, we have an opt-out rate of 4.8%; well exceeding the goal of less than 25%.</p><figure><img alt="opt-out rate number" src="https://cdn-images-1.medium.com/max/1024/1*e9oMGBuk8no11KQU2hTNGA.png" /><figcaption>*Excludes White Label and Multi-Tenant Accounts</figcaption></figure><p>The numbers don’t lie. And, neither does the feedback. Here’s a quote from an enterprise customer:</p><blockquote>I do love the changes you guys made [to the navigation] in the platform. It looks so much better. It’s so much more revamped. It’s clean, I like it. I think it’s sick, in the best way possible.</blockquote><p>Given that we achieved our success metric, what’s next for the new navigation?</p><figure><img alt="new navigation — main (top), settings (bottom)" src="https://cdn-images-1.medium.com/max/1024/1*VizLNeFdnMGdSxfwNaVnfg.png" /></figure><h4><strong>Looking Forward</strong></h4><p>We’re an ambitious, growing design team in a small company that’s always challenging the status quo. One of our company values is asking “what if?”. This navigation redesign unified the design team. It coincided with each designer’s onboarding. We are grateful to have had the opportunity to bond as a team, provide value to the business, and help users decrease their time on task.</p><p>Yet, design is never done. As such, we have several next steps we’d like to complete.</p><p><strong>Next steps:</strong></p><ul><li>Make the new navigation the default experience for all users (i.e., remove the ability to opt out)</li><li>Release the new navigation to White Label and Multi-Tenant Accounts</li><li>Establish guidelines for whether a new feature goes in the Main section or Settings section of the new navigation</li><li>Add global collapse and expand functionality to the new navigation</li><li>Move review-related actions closer in context to the Reviews page</li></ul><h4><strong>Parting Thought</strong></h4><p>It has taken a lot of work to get to this point. We’re excited about the new changes we’ve made and the future problems we’ll get to solve as our company grows. Thanks to ReviewTrackers users for providing valuable feedback. They helped make this redesign possible.</p><p><em>We’re always interested in growing our team and hearing from talented designers. If you have any questions or would like to get in touch, please reach out to </em><a href="/service/mailto:product@reviewtrackers.com"><em>product@reviewtrackers.com</em></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=483b70345bbf" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/rethinking-navigation-at-reviewtrackers-483b70345bbf">Rethinking Navigation at ReviewTrackers</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Getting Principled at ReviewTrackers]]></title>
            <link>https://medium.com/reviewtrackers-engineering/getting-principled-at-reviewtrackers-bafe9b3c12f6?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/bafe9b3c12f6</guid>
            <category><![CDATA[user-experience]]></category>
            <category><![CDATA[design]]></category>
            <category><![CDATA[design-thinking]]></category>
            <category><![CDATA[ux]]></category>
            <category><![CDATA[product-design]]></category>
            <dc:creator><![CDATA[John Orzechowski]]></dc:creator>
            <pubDate>Mon, 11 Jul 2022 17:16:48 GMT</pubDate>
            <atom:updated>2023-12-13T20:53:39.277Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5M6iuLolM_STTHn2Tdbz7A.png" /></figure><h4>Written by <a href="https://www.linkedin.com/in/johnorzechowskijr/">John Orzechowski</a></h4><p>Products are a snapshot of a collection of decisions made over time. As products evolve, unguided decision making can turn them into a mess. There might be differing amounts of user guidance, components, and themes. As designers, we have the power to both create and stop said mess.</p><p>At ReviewTrackers, we grow constantly, collaborate consistently, and ship quickly. This is what I define as our competitive edge. This fast-paced nature allows us to learn and do a lot, but continued success is dependent on having guiding principles.</p><p>How do we avoid unguided decision making? The answer is design principles.</p><h4><strong>Why we need principles</strong></h4><p>Since June 2021, the RT design team has quintupled in size (1 → 5 designers)!</p><figure><img alt="ReviewTrackers Design Team growth from June 2021 (John — 1 designer) to July 2022 (John, Cici, Jake, Renee, Jordan — 5 designers)" src="https://cdn-images-1.medium.com/max/1024/1*uOpxbEzr9EBhzP9IQLkjaA.png" /><figcaption>ReviewTrackers Design Team</figcaption></figure><p>Our design team growth happened in progressive stages over the course of one year. <a href="https://www.linkedin.com/in/cecilia-xie">Cici</a> was brought on board, then <a href="http://linkedin.com/in/jacobcarterdesign">Jake</a>, then <a href="https://www.linkedin.com/in/reneepatel">Renee</a>, and then <a href="https://www.linkedin.com/in/jordasant9">Jordan</a>. Each of these individuals bring unique perspectives and skills to the team. While we had an onboarding process, we were lacking guidance on how we view design at RT. This resulted in each person having their own interpretation of successful output.</p><p>From 2019 until now (July 2022), we have created a component library (recently upgraded to v2), written component documentation, defined a type scale, spacing system, and tied that all together to <a href="https://engineering.reviewtrackers.com/establishing-a-design-system-at-reviewtrackers-4dc683270d59">establish a design system at ReviewTrackers</a>. Despite all this, we were still missing ‘something’. That ‘something’ is principles. A tool to help guide decision making around and application of the design system in our daily work.</p><p>To set each designer up for success, we needed to agree on our core beliefs. These beliefs serve as a guide (but do not dictate) for how we think about design at RT. They hold us accountable and help us deliver on our promise to users.</p><p>We use these principles to evaluate historical, assess current, and plan future decisions.</p><p>What if I told you that you could create principles through an easy-to-follow process?</p><h4><strong>The process for creating principles</strong></h4><p>We mixed a bottom-up and top-down approach to inform these principles.</p><ol><li>Analyzed user interview notes, strategy documents, mission/vision statements</li><li>Created a single source-of-truth document with draft principles</li><li>Added <em>many</em> comments in said document</li><li>Held discussion in comments and debated in meetings</li><li>Ranked, merged, and consolidated principles</li><li>Repeated steps 3 and 4 until agreement was reached</li></ol><p>As a result, we landed on four design principles.</p><h4><strong>The principles</strong></h4><p>We value being easily understood, so each principle is in plain English with an average Flesch Reading Ease score of 77.</p><p>That way, they are inviting to all audiences; both inside and outside of RT.</p><p><em>Design inclusively<br></em>Meet each person where they are and give them an equal chance in managing their customers’ experience. Design for one and extend to all; make sure things are simple by default and complex by opt-in.</p><p><em>Nudge towards success<br></em>Let people feel one step ahead. Favor guidance over choice and be ok with deciding for them.</p><p><em>Promote collaboration<br></em>More often than not, there are many people in a company using RT to achieve a shared goal. Make them feel as part of a team in the product as they are outside of it.</p><p><em>Embrace less is more<br></em>Remove when you are able to, replace if you can, and add if you must. When replacing or adding, be minimal.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VkOzksyaERrv4dd7FnUV0A.png" /><figcaption>Design Principles Asset</figcaption></figure><p>Now that we had our principles created, how did we ensure they lived beyond a Google doc?</p><h4><strong>Applying the principles</strong></h4><p>At RT, we use Figma. We turned the principles into a component placed within our design system. This is to remind ourselves of them at every stage of our work. We add them to design files we are working on using the keyboard shortcut “Shift + I”.</p><p>After the principles were established, we held a one-month trial period to apply them in our weekly Design critiques. This allowed us to develop the muscle memory of using them in conversation.</p><p>One of our designers, Cici Xie, was working on our product’s Local Search feature. It provides users with insight on how their business ranks on Google for different search terms. Through consistent reference of principles, she guided the design to a final iteration.</p><p>This is one example of the team using principles to prevent unguided decision making. Resulting in more productive design critiques, collaboration on a vision, and higher-quality product.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e_uf65XhxeQqCaW0HlBbjw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BPuMCRGtQIaskD1jHEGWiA.png" /><figcaption>Local Search (Before and After)</figcaption></figure><h4><strong>Parting thought</strong></h4><p>Design is never done and neither are principles. These will guide our thinking six months, one year, etc. from now. Yet, we recognize they will evolve as new user problems emerge and Design grows at RT. The important thing is that we now have a framework in place and can iterate as needed.</p><p><em>We’re always interested in growing our team and hearing from talented designers. If you have any questions or would like to get in touch, please reach out to </em><a href="/service/mailto:product@reviewtrackers.com"><em>product@reviewtrackers.com</em></a></p><h4>Artificial intelligence (AI) principles</h4><p>In H2 2023 (July 2023 — December 2023), the Design team had an AI-focused objective:</p><blockquote>Enable the team to design effective interactions for AI to provide a highly-valuable product to our users.</blockquote><p>Knowing the importance of design principles, we followed a similar approach to create ones that were AI focused.</p><p><em>Spell it out<br></em>Be clear about what AI is and how it is used in the product. Allow for the correction of AI-generated content.</p><p><em>Design for all skill levels<br></em>Design so that all people benefit from AI. Allow people to choose the type of relationship they want to have with AI.</p><p><em>Support the V formation<br></em>AI must help a person solve existing problems, not create new ones. AI is meant to streamline their ability to do this.</p><p><em>Mindfulness matters<br></em>Be mindful about where and when AI makes a difference. Both to a person’s experience and the product’s value.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a0KX_HJ-RoaO4E7UzCelog.png" /><figcaption>AI Design Principles Asset</figcaption></figure><p>An example of these AI principles in action is our product’s AI Responding feature. It gives users a review response with the click of a button.</p><figure><img alt="AI Responding feature" src="https://cdn-images-1.medium.com/max/1024/1*U7o1MIewOgS-v8IbOu3VwQ.png" /><figcaption>AI Responding</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bafe9b3c12f6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/getting-principled-at-reviewtrackers-bafe9b3c12f6">Getting Principled at ReviewTrackers</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Establishing a Design System at ReviewTrackers]]></title>
            <link>https://medium.com/reviewtrackers-engineering/establishing-a-design-system-at-reviewtrackers-4dc683270d59?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/4dc683270d59</guid>
            <category><![CDATA[ui]]></category>
            <category><![CDATA[ux]]></category>
            <category><![CDATA[product-design]]></category>
            <category><![CDATA[design-systems]]></category>
            <category><![CDATA[design]]></category>
            <dc:creator><![CDATA[John Orzechowski]]></dc:creator>
            <pubDate>Wed, 02 Jun 2021 14:37:08 GMT</pubDate>
            <atom:updated>2021-06-02T14:37:08.331Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Banner image of the design system" src="https://cdn-images-1.medium.com/max/1024/1*rrjnqNTmk6S8LYWQjD1hnA.png" /></figure><h4>Co-written by <a href="https://www.linkedin.com/in/johnorzechowskijr/">John Orzechowski</a> and <a href="https://www.linkedin.com/in/jenniferzettinger/">Jennifer Zettinger</a></h4><p>This article talks about the importance of establishing a design system at ReviewTrackers. First, we’ll explain why a design system is important and how the need was identified. Then, we’ll cover the process the design team went through to get the first version up and running. We’ll finish up with some next steps, as our goal is to always be improving.</p><h3>Why is a design system important?</h3><h4>Overview</h4><p>In a company with several departments, designers stationed on different feature teams, and an embodied company value to “<em>set each other up for success</em>”, there needs to be a shared resource that the organization can reference for how ReviewTrackers approaches design. This role is assumed by a design system.</p><p>A design system’s purpose is to cross-functionally unify design language, thought, and application within the product. It focuses on providing guidance and answering the following: <em>how does ReviewTrackers talk about, think about, and apply design?</em></p><p>This is a question the design team inherently embodies and understands, day-to-day, but the system acts as a referenceable, supporting structure for colleagues to learn. Not only does this provide a resource to internal teams, but this question and its answers directly impact the experience users have with the product.</p><p>A successful design system requires several parts.</p><p><strong>Component library: </strong>The entire collection and documentation of UI components used in a product.</p><figure><img alt="An image of the component library" src="https://cdn-images-1.medium.com/max/1024/1*N_OWWflLPoa-7UB-QWfLQA.png" /><figcaption>Component Library</figcaption></figure><p><strong>Component documentation: </strong>The entire collection of “<em>what, why, when, where, and how?</em>” for foundations and components used in a product. This includes content guidelines.</p><figure><img alt="An image of component documentation" src="https://cdn-images-1.medium.com/max/1024/1*GVYRcc8YM47sAmOCUauneQ.png" /><figcaption>Component Documentation</figcaption></figure><p><strong>Color palette: </strong>The entire collection and documentation of visual identity used in a product.</p><figure><img alt="An image of the color palette" src="https://cdn-images-1.medium.com/max/1024/1*T1xN1sq1K7aI8Z1oAPjKdg.png" /><figcaption>Color Palette</figcaption></figure><p><strong>Type scale: </strong>The entire collection and documentation of typography used in a product.</p><figure><img alt="An image of the version 1 type scale" src="https://cdn-images-1.medium.com/max/1024/1*l2S_v0XCpeSlLTpkn2o2uw.png" /><figcaption>v1 Type Scale</figcaption></figure><p><strong>Spacing: </strong>The entire collection of rules for spacing used in a product.</p><figure><img alt="An image of the linear spacing system" src="https://cdn-images-1.medium.com/max/1024/1*NcYhNkKRvZVAn0yIiRcnUw.png" /><figcaption>Linear Spacing System</figcaption></figure><h4>Defining success</h4><p>ReviewTrackers believes in measuring success and using data to inform stories. Therefore, success with a design system at ReviewTrackers is broken down into two parts, qualitative and quantitative. We’ve currently achieved qualitative success but will need more time to measure quantitative success.</p><p>Qualitative:</p><ul><li>Provide every Product and Engineering employee with access to the design system</li><li>Learn how to use GitHub for component documentation contribution</li></ul><figure><img alt="An image of objective key results (OKR)" src="https://cdn-images-1.medium.com/max/1024/1*2mlNrarXACaSRZZzzUFQeQ.png" /><figcaption>OKR Example</figcaption></figure><p>Quantitative:</p><ul><li>Reduce design-QA time necessary to bring a feature to market</li><li>Reduce design deliverable process time</li><li>Increase efficiency between design and engineering</li><li>Increase ‘System Usability Scale’ (before versus after design system)</li><li>Increase ‘Net Promoter Score’ (before versus after design system)</li></ul><h3>What process did we take to create a design system?</h3><h4>Establishing a foundation</h4><p>Building a design system takes time. It was important to clearly define our starting point. To do this, an in-depth UX and UI audit was performed on every page of the ReviewTrackers platform to identify patterns and inconsistencies with existing components.</p><p>Next, a library of reusable components was created in Figma for the design team to utilize. This allowed for consistency in design deliverables and avoided additional design debt while the creation of the design system took place.</p><figure><img alt="An image of the interface inventory" src="https://cdn-images-1.medium.com/max/1024/1*tD9_ruCubAjtdQatwmIvVA.png" /><figcaption>Interface Inventory</figcaption></figure><h4>Getting people excited</h4><p>At ReviewTrackers, we have monthly Product and Engineering lunch and learns, which allows employees to share role knowledge with their colleagues.</p><p>Given that the audience for a design system is multi-disciplinary, it’s important to take this opportunity to spread design-thought leadership.</p><p>Since the design team’s initial focus was on the component library, this is what we presented. We shared a brief overview of what a design system is, what a component library is, and our process for integrating a component library into the larger design system. We had a total of 20 attendees!</p><figure><img alt="An image of the lunch and learn agenda from July 2020" src="https://cdn-images-1.medium.com/max/1024/1*XNuENAMInbwShx2eMN0YTQ.png" /><figcaption>Lunch and Learn Agenda — July 2020</figcaption></figure><h4>Setting ourselves up for success</h4><p>Once we had our component library established and shared cross-functionally with colleagues, it was time to jump into the design system. The design team had a brainstorming session to identify a list of components to include in the first iteration. The components were added as cards in a shared design team Trello board.</p><figure><img alt="An image of the design team’s Trello board" src="https://cdn-images-1.medium.com/max/1024/1*5jvBLUISfk6D2dVZiU83OA.png" /><figcaption>Trello Board</figcaption></figure><p>Next, to help inspire and influence, we explored other design systems to learn about the approach, format, and structure others took in an effort to determine what would work best for us. From this exercise, we learned that design systems typically share the following characteristics:</p><ul><li>Brand outline</li><li>Foundation/Principles</li><li>Components</li><li>Content/Writing</li><li>Search capability</li><li>Content all follows a similar do/don’t format</li><li>Green and red accents to help visually denote the do/don’t concept</li></ul><p>As a result, we applied the UX heuristic <a href="https://lawsofux.com/jakobs-law/">Jakob’s Law</a> and ensured our component documentation followed similar principles.</p><p>We created standard templates for how we would construct our component documentation to ensure each piece followed the same approach. From here, we divided and conquered.</p><ol><li>A designer would choose a component. Using the template, they’d write documentation for that component.</li><li>In an effort to always be improving, we’d note areas of improvement along the way and document design debt.</li><li>Once the documentation was finished, it would be added to a shared team Google doc. Another designer would review for content and a second designer would review for the overall format, tone, and voice.</li></ol><p>It took about 6 months to complete this process for 19 total components.</p><figure><img alt="An image of the component documentation template" src="https://cdn-images-1.medium.com/max/1024/1*0XPwTVW7SP8espZsBsPFVg.png" /><figcaption>Component Documentation Examples</figcaption></figure><h4>Putting the design system into motion</h4><p>The design system is meant to be a shared source of truth for designers, product managers, software engineers, and QA engineers to reference in their day-to-day work. With documentation completed, the next step was to determine where this shared source of truth would live.</p><p>After brainstorming as a team, we determined Storybook was the best location for the design system, an internal resource engineering uses for components. The design team shared this idea with the engineering team and they were immediately on board.</p><p>We worked with a lead front-end engineer to establish a process for designers to contribute design system documentation to Storybook. This resulted in designers getting access to Github, learning how to create a markdown file, and submit a pull request. We created a peer review system and had proper checks in place.</p><figure><img alt="An image of markdown used in GitHub" src="https://cdn-images-1.medium.com/max/1024/1*EvdxvDhEMynIfczbAwqfnQ.png" /><figcaption>GitHub Markdown</figcaption></figure><h4>Integrating the design system into standard processes</h4><p>The design team shared what we accomplished at “Team Demo Days”, a dedicated time for the product and engineering org to come together and share what they’ve been working on. With design system documentation available for anyone at the company to use, the next step was to integrate it into standard processes. With consistent knowledge sharing, regularly referencing it as a source of truth, and bringing increased visibility to the guidelines and best practices that were established in the design system, the hope is that it will become an integral point of reference in the team’s daily workflow.</p><figure><img alt="An image of component documentation in Storybook" src="https://cdn-images-1.medium.com/max/1024/1*EFS-n-WzQni8Ll9DFcsVBQ.png" /><figcaption>Component Documentation in Storybook</figcaption></figure><h3>Looking forward</h3><p>Design is never done, and therefore, neither is a design system. What does the design team have in store for the second iteration?</p><p><strong>Immediate next steps:</strong></p><h4><strong>Design &lt;&gt; Engineering</strong></h4><p>Our engineering colleagues have been bought into the design system since day one. They’re invaluable resources when wanting to learn how design can continue to foster strong design &lt;&gt; engineering relationships.</p><p>Through bi-weekly meetings with engineering, the design team learns to understand engineering pain points in the front-end development process, translate those to problem statements, and determine how to make updates to our system.</p><h4>Type scale</h4><p>In the current iteration of the design system (component library), design worked to replicate the v1 type scale front-end engineers use in the product (size, weight, etc.)</p><p>However, we learned that there’s an opportunity to create a new type scale with engineering. This is because we uncovered v1 is not a well-defined scale, legacy, and front-end engineering also has frustrations with it.</p><p>We’re currently working on implementing our v2 type scale within the product.</p><figure><img alt="An image of the version 2 type scale" src="https://cdn-images-1.medium.com/max/1024/1*cvOxTsazsFRSQUmigvdF9Q.png" /><figcaption>v2 Type Scale</figcaption></figure><h4>Spacing system</h4><p>We uncovered that a large pain point is the lack of defined, shared spacing rules between design and engineering. Design is leading the charge on what would afford the best experience for users, where engineering is sharing perspective on how they apply this concept within code.</p><p>We’re currently working on implementing our linear spacing system within the product and applying a mutually understood nomenclature to spacers.</p><p>From this work, a future iteration could be providing different layouts within the product (i.e., similar to how Gmail offers Compact, Comfy, Cozy).</p><h4>Sharing component documentation</h4><p>Now that component documentation is live (i.e., beyond a private Google doc) and exists within Storybook, it’s time to share the resource with the broader organization. This is important for several reasons:</p><ol><li>Promotes design within the organization</li><li>Allows all departments to speak the same design language</li><li>Encourages growth, feedback, and insight to inform existing gaps or improvement areas with our system</li></ol><p>Additionally, the reason why the design team chose Storybook was that it already serves as a cross-functional tool, so we’re seamlessly integrating it into that workflow.</p><p><strong>Future next steps:</strong></p><h4>Having code associated with each component</h4><p>An ideal state would have code related to each component in the design system, so engineering can “grab and go” with components. This would also offer a path for a consistent design system &lt;&gt; code parity.</p><h4>Making the design system public-facing</h4><p>To expand the reach of ReviewTrackers design, we have aspirations to eventually have this resource in a public-facing environment.</p><h4>Injecting product principles into the design system</h4><p>Design and product are working together to clearly define product principles, which will help inform future iterations of the design system to align with said principles.</p><p>Rather than the design system being seen as a separate entity, it needs to be seen as an extension of product principles. This will strengthen the system.</p><h3>Before you go</h3><p>Getting to this point has taken a lot of work, but we’re incredibly excited to build on the foundation we’ve created and further empower users, colleagues, and ourselves!</p><p><em>We’re always interested in growing our team and hearing from talented designers. If you have any questions or would like to get in touch, please reach out to </em><a href="/service/mailto:product@reviewtrackers.com"><em>product@reviewtrackers.com</em></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4dc683270d59" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/establishing-a-design-system-at-reviewtrackers-4dc683270d59">Establishing a Design System at ReviewTrackers</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Cutting-Edge Model for Aspect-Based Sentiment Analysis]]></title>
            <link>https://medium.com/reviewtrackers-engineering/building-a-cutting-edge-model-for-aspect-based-sentiment-analysis-9dc5412ad1a9?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/9dc5412ad1a9</guid>
            <category><![CDATA[bert]]></category>
            <category><![CDATA[deep-learning]]></category>
            <category><![CDATA[sentiment-analysis]]></category>
            <category><![CDATA[hugging-face]]></category>
            <category><![CDATA[absa]]></category>
            <dc:creator><![CDATA[Andrew Johnson]]></dc:creator>
            <pubDate>Fri, 19 Feb 2021 16:00:14 GMT</pubDate>
            <atom:updated>2021-02-19T16:00:14.486Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://www.reviewtrackers.com/">ReviewTrackers</a>’ Insights engine pulls data from millions of reviews for our customers. One major component of this is predicting the sentiment of individual keywords. In this post, I will go over how to build a <strong>keyword-level sentiment analysis engine</strong> <strong>using state of the art technology</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jC2oiZvKgyVnOHD1uU8v0Q.png" /></figure><h3><strong>Part 1: Defining the Task</strong></h3><h3><strong>What is Sentiment Analysis?</strong></h3><p><strong>Sentiment Analysis</strong> is a task in natural language processing (NLP) in which a piece of text is assigned a sentiment prediction. At its simplest, this could mean classifying an entire sentence as positive or negative. Let <em>x</em> be the input the model receives (text that has been segmented into individual tokens) and <em>y</em> be the desired output (our sentiment label):</p><pre>x = [&#39;I&#39;, &#39;loved&#39;, &#39;the&#39;, &#39;food&#39;, &#39;!&#39;]<br>y = &#39;positive&#39;</pre><p>However, natural language is wonderfully diverse and messy, so for many applications a more fine-grained approach is more informative. Consider the example below:</p><pre>x = [&#39;I&#39;, &#39;loved&#39;, &#39;how&#39;, &#39;flavorful&#39;, &#39;the&#39;, &#39;burger&#39;, <br>     &#39;was&#39;, &#39;,&#39;, &#39;but&#39;, &#39;wish&#39;, &#39;the&#39;, &#39;customer&#39;, <br>     &#39;service&#39;, &#39;was&#39;, &#39;faster&#39;, &#39;.&#39;]<br>y = &#39;mixed&#39;</pre><p>This text chunk contains multiple different sentiments within the same sentence. Here, a one-sentiment-fits-all model is very limited. One compromise is to predict the frustratingly uninformative label “mixed” sentiment. Ideally, we would want our model to predict that within that sentence, “burger” has positive sentiment and “customer service” has negative sentiment.</p><h3><strong>What is ABSA?</strong></h3><p><strong>Aspect-Based Sentiment Analysis</strong> (ABSA) is one way to address the problem above. In this version of the task, sentiment predictions are made for every “aspect” in the text, as shown below:</p><pre>x =([&#39;burger&#39;],<br>    [&#39;I&#39;, &#39;loved&#39;, &#39;how&#39;, &#39;flavorful&#39;, &#39;the&#39;, &#39;burger&#39;, <br>     &#39;was&#39;, &#39;,&#39;, &#39;but&#39;, &#39;wish&#39;, &#39;the&#39;, &#39;customer&#39;, <br>     &#39;service&#39;, &#39;was&#39;, &#39;faster&#39;, &#39;.&#39;])<br>y = &#39;positive&#39;</pre><pre>x =([&#39;customer&#39;, &#39;service&#39;],<br>    [&#39;I&#39;, &#39;loved&#39;, &#39;how&#39;, &#39;flavorful&#39;, &#39;the&#39;, &#39;burger&#39;, <br>     &#39;was&#39;, &#39;,&#39;, &#39;but&#39;, &#39;wish&#39;, &#39;the&#39;, &#39;customer&#39;, <br>     &#39;service&#39;, &#39;was&#39;, &#39;faster&#39;, &#39;.&#39;])<br>y = &#39;negative&#39;</pre><p>Note that in practice, this is often part of a larger pipeline in which a step called “<strong>aspect extraction</strong>” is applied before ABSA to determine which words in the text should be treated as aspects. Aspect extraction might be conceptualized as a sequence labeling task. Here, each token is predicted to be <em>‘B’</em> for the beginning token of an aspect,<em> ‘I’</em> for any intermediate token in a multi-token aspect or <em>‘O’ </em>for other:</p><pre>x = [&#39;I&#39;, &#39;loved&#39;, &#39;how&#39;, &#39;flavorful&#39;, &#39;the&#39;, &#39;burger&#39;, <br>     &#39;was&#39;, &#39;,&#39;, &#39;but&#39;, &#39;wish&#39;, &#39;the&#39;, &#39;customer&#39;, <br>     &#39;service&#39;, &#39;was&#39;, &#39;faster&#39;, &#39;.&#39;]<br>y = [&#39;O&#39;, &#39;O&#39;, &#39;O&#39;, &#39;O&#39;, &#39;O&#39;, &#39;B&#39;, &#39;O&#39; ,&#39;O&#39;, &#39;O&#39;, &#39;O&#39;, <br>     &#39;O&#39;, &#39;B&#39;, &#39;I&#39;, &#39;O&#39;, &#39;O&#39;, &#39;O&#39;]</pre><h3><strong>Part 2: Defining the Model</strong></h3><h3><strong>BERT</strong></h3><p>Assuming we have our text and its aspects extracted, how do we train the model for ABSA? Here, we will start by using <a href="https://arxiv.org/pdf/1810.04805.pdf">BERT</a>, a versatile, pre-trained language model.</p><p>A <strong>language model </strong>(LM) assigns a probability to a combination of tokens. For example, reasonable LM should give a higher likelihood to <em>[‘turn’, ‘right’]</em> than <em>[‘turn’, ‘write’]</em>. This deceptively simple framework is what allows voice recognition software to use context to distinguish similar sounding words. LMs also allow you to make predictions: out of all words in the model vocabulary, which is the most likely to come next in the sequence <em>[‘take’, ‘it’, ‘or’, ‘leave’]</em>? A good LM would probably select <em>‘it’</em> as the next token.</p><p>If we were starting from scratch with randomly initialized parameters, we would in effect have to teach the LM the basics of the language just to get started. Luckily, BERT is already <strong>pre-trained</strong> on massive amounts of data (all of English Wikipedia, plus a large corpus of books).</p><p>For many machine learning tasks, labeled training data is needed. Training using manually labeled data is referred to as <strong>supervised learning</strong>. However, data annotation is time-consuming and unrealistic for the scale of data that BERT is trained on. To get around this, BERT is trained using a clever approach called <strong>self-supervised learning</strong>. One self-supervised training task is called <em>masked language modeling</em>. In this task, a certain token is masked out and the model tries to predict what this missing word is. Let’s say that the sentence “The roast beef was tasty.” occurs in the dataset. From that, a training pair can be automatically generated without any manual annotations:</p><pre>x = [&#39;The&#39;, &#39;roast&#39;, &#39;[MASK]&#39;, &#39;was&#39; &#39;tasty&#39;, &#39;.&#39;]<br>y = [&#39;beef&#39;]</pre><p>You can play around with the BERT masked language model <a href="https://huggingface.co/bert-base-uncased">here</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BJxJJIvFZA26xdA0" /></figure><h3><strong>Post-Training</strong></h3><p>This step is optional. If you skip directly to the fine-tuning step, you can still have a high-functioning model. However, post-training has been shown to help. <a href="https://www.aclweb.org/anthology/N19-1242.pdf">Xu et al. (2019)</a> demonstrate improved performance of BERT on ABSA for online reviews by post-training it on data from that domain. <strong>Post-training</strong> simply means initializing the model with BERT’s pre-trained weights and continuing the same self-supervised training regime on a more specific dataset. For example, if your downstream task covers healthcare and you have access to a large amount of unannotated healthcare data, this may be a good step to take.</p><p><a href="https://huggingface.co/activebus/BERT_Review">Here</a> is an interactive example of Xu et al.’s model post-trained on online reviews:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*iaQiME1-ZamH1Xty" /></figure><h3><strong>Fine-Tuning</strong></h3><p>So far, we have only used BERT as a language model, but ultimately we need it to predict sentiment polarity, not word likelihood. Adapting the architecture for sequence classification and running fine-tuning allows us to do this. First, we add a <strong>classification layer</strong> on top of the language model. Mathematically, this functions as follows:</p><blockquote>P = softmax(CW<em>ᵀ</em>),</blockquote><blockquote>where C <em>∊</em> ℝ<em>ᴴ</em> and W <em>∊</em> ℝ<em>ᴷˣᴴ</em></blockquote><p>Let’s unpack that. <em>C</em> is the output of the final hidden layer of the LM from the special classification token (more on that later). The notation above indicates that <em>C</em> is composed of real numbers ℝ and its shape is that of <em>H</em>, i.e. the size of the previous hidden layer. In the case of the base model of BERT, <em>H</em>=768. <em>W</em> refers to the weights of the new classification layer, which will be updated during fine-tuning. It is a tensor of dimensionality <em>K</em> by <em>H</em>, where <em>K</em> is the number of classes we are predicting across. Since we are using a three-way distinction of positive/neutral/negative, <em>K</em>=3.</p><p>Taking the dot product of <em>C</em> and the transposition of <em>W</em> results in an array of size <em>K</em>. These are sometimes referred to as “logits”. These values need to be normalized to have a probability distribution that sums to 1. The <a href="https://en.wikipedia.org/wiki/Softmax_function">softmax</a> function does this for us. To get the predicted label, just return the label corresponding to the highest probability:</p><blockquote>y_hat = labels[argmax(P)]</blockquote><p><strong>🤗 </strong>Hugging Face’s <a href="https://github.com/huggingface/transformers">transformers</a> library in PyTorch makes it easy to import BERT with this classification layer already on top:</p><pre>from transformers import BertForSequenceClassification</pre><pre>model = BertForSequenceClassification.from_pretrained(&quot;bert-base-uncased&quot;, num_labels=len(labels))</pre><p>This framework also makes it quick and easy to try out different architectures and flavors or BERT. Think <a href="https://ai.facebook.com/blog/roberta-an-optimized-method-for-pretraining-self-supervised-nlp-systems/">RoBERTa</a> might boost performance?</p><pre>from transformers import RobertaForSequenceClassification</pre><pre>model = RobertaForSequenceClassification.from_pretrained(&quot;roberta-base&quot;, num_labels=len(labels))</pre><p>Want to use the <a href="https://huggingface.co/activebus/BERT_Review">version</a> of BERT that is already post-trained on online reviews?</p><pre>from transformers import BertForSequenceClassification</pre><pre>model = BertForSequenceClassification.from_pretrained(&quot;activebus/BERT_Review&quot;, num_labels=len(labels))</pre><p>Want a more <a href="https://medium.com/huggingface/distilbert-8cf3380435b5">light-weight model</a> that can run inference more quickly?</p><pre>from transformers import DistilBertForSequenceClassification</pre><pre>model = DistilBertForSequenceClassification.from_pretrained(&quot;distilbert-base-uncased&quot;, num_labels=len(labels))</pre><p>etc. How convenient!</p><p>Our model is now structured to assign a probability distribution over our label set. Because of the nature of the task, <strong>annotated training data</strong> is required, making this a supervised learning problem. There are <a href="https://kaijuneer.medium.com/fine-tuning-tensorflow-bert-model-for-sentiment-analysis-1119fd6bef49">plenty</a> <a href="https://towardsdatascience.com/sentiment-analysis-in-10-minutes-with-bert-and-hugging-face-294e8a04b671">of</a> <a href="https://curiousily.com/posts/sentiment-analysis-with-bert-and-hugging-face-using-pytorch-and-python/">guides</a> with sample code available to show to to fine-tune BERT for sentiment analysis. Again, using PyTorch and Hugging Face make this a relatively easy process.</p><h3><strong>Structuring the Input</strong></h3><p>As for how to include the aspect in the input, several leading models use concatenation, such as prefixing the aspect before the whole text with a separation marker in between. For example, you could structure the input as follows:</p><pre>x = [&#39;[CLS]&#39;, &#39;customer&#39;, &#39;service&#39;, &#39;[SEP]&#39;, &#39;I&#39;, &#39;loved&#39;, <br>     &#39;how&#39;, &#39;flavorful&#39;, &#39;the&#39;, &#39;burger&#39;, &#39;was&#39;, &#39;,&#39;, &#39;but&#39;, <br>     &#39;wish&#39;, &#39;the&#39;, &#39;customer&#39;, &#39;service&#39;, &#39;was&#39;, &#39;faster&#39;, <br>     &#39;.&#39;, &#39;[SEP]&#39;]</pre><p>Here,<em> ‘[CLS]’ </em>and <em>‘[SEP]’</em> are special tokens that BERT uses to denote <strong>sentence classification </strong>and <strong>sentence separators</strong>, respectively. The classification token is the token whose weights are fed into the classification layer of the network. The separator token is often used for completely different NLP tasks where the input consists of two distinct parts (next sentence prediction, question answering, etc.), but BERT is adaptable enough to learn from the fine-tuning data that this structure is used here to indicate the aspect to focus on for sentiment prediction.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/744/0*6yTxPBlLSk0lm-ja" /><figcaption><em>Illustration of BERT input for ABSA from Xu et al.</em></figcaption></figure><p>With this (and some training data and processing power), you should have all the parts necessary to make a strong ABSA model. Here is an overview of the training stages. As a reminder, pre-training has already been done for us, and post-training is optional:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QE_3XbtxgZRjsrLBMQITgw.png" /></figure><p>If you want to compare your model to others, <a href="https://alt.qcri.org/semeval2014/task4/">SemEval 2014 Task 4 Subtask 2</a> is a common evaluation benchmark. As of early 2021, leading papers with more complex architectures (<a href="https://arxiv.org/pdf/1908.11860.pdf">Rietzler et al.</a>, <a href="https://arxiv.org/pdf/2005.05635.pdf">Tian et al.</a>, etc.) have pushed the bar up to just a few percentage points shy of 90% accuracy on that dataset. Best of luck building your own model and pushing the bar even higher.</p><p>To learn more about some of the other NLP tasks that <a href="https://www.reviewtrackers.com/">ReviewTrackers</a> handles, check out our <a href="https://www.reviewtrackers.com/reports/natural-language-processing/">white paper</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9dc5412ad1a9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/building-a-cutting-edge-model-for-aspect-based-sentiment-analysis-9dc5412ad1a9">Building a Cutting-Edge Model for Aspect-Based Sentiment Analysis</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Visualizing Load Test Data with Locust and Grafana]]></title>
            <link>https://medium.com/reviewtrackers-engineering/visualizing-load-test-data-with-locust-and-grafana-da627036c2c6?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/da627036c2c6</guid>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[visualization]]></category>
            <category><![CDATA[grafana]]></category>
            <category><![CDATA[load-testing]]></category>
            <category><![CDATA[locust]]></category>
            <dc:creator><![CDATA[Zach Pierce]]></dc:creator>
            <pubDate>Wed, 02 Sep 2020 16:16:16 GMT</pubDate>
            <atom:updated>2020-09-02T16:16:16.448Z</atom:updated>
            <content:encoded><![CDATA[<p>For information about <em>how</em> we configured our load tests, see <a href="https://engineering.reviewtrackers.com/our-load-testing-journey-with-locust-c599670bc41b">this article</a> from fellow Trakkie, <a href="https://medium.com/u/1ffcaee2d590">zach attas</a>. Since Zach discussed how we defined our tests, I’ll discuss how we integrated those into our lower environments in order to obtain meaningful data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*B3xpuWhWueqo1jrFy3dEFg.gif" /><figcaption>Time-series load test data visualized in Grafana</figcaption></figure><h3>How we run our tests in CI</h3><p>In our test environment, we stand up ephemeral copies of our development stack for both our integration and QA testing. The methods behind this are subject enough for their own dedicated blog post, but essentially engineers pass in their branch name, and we deploy a namespaced version of our full stack built around that branch into our Kubernetes cluster. QA tests receive ingress objects in order for the developer to connect via an internal URL, i.e. app.qa-1234.internaldomain.com, whereas integration tests run within the namespace, and connect directly to service endpoints using the cluster FQDN, i.e. app.qa-1234.cluster.svc.local (so that kube-proxy handles the networking, saving on networking costs).</p><p>For load testing, we do very much the same as we do for integration tests - that is to say, deploy a stack based around the requested branch sans ingress. Since we don’t yet use our Kubernetes infrastructure for our higher environments, there’s no need to add that layer of obscurity to test results. While that ephemeral environment is being stood up, we also build out a very simply Docker container based off the <a href="https://hub.docker.com/_/python">official Python Docker container</a> for our Locust tests. <a href="https://docs.locust.io/en/stable/running-locust-docker.html">Locust provides documentation</a> for running the tests from a Docker container, so I won’t get too much into that.</p><p>I will say though, in our CI testing we did not find it necessary to run a distributed version of Locust. While we did develop the capability to run Locust in a distributed fashion for these tests, we found that a single Docker container on a Jenkins drone with 2CPU/8Gi RAM was more than enough to run our test suite while simulating thousands of users.</p><p>Once the stack has been stood up and the test container built, we trigger the tests, passing in user-defined parameters like user count, runtime, and hatch rate as environment variables. Although we’re running the tests in a Kubernetes cluster, we elected to invoke the tests by utilizing Docker in Docker as described by fellow Trakkie <a href="https://medium.com/u/1d55ee1abbe3">David Kirk</a> <a href="https://engineering.reviewtrackers.com/kubernetes-in-cluster-docker-image-builds-3ae216804815">in his previous post here</a>. Aside from simplicity and consistency with our other test methods, we did find running the tests in DinD to be much quicker than running it via a Kubernetes job.</p><h3>Retaining time-series data from test runs</h3><p>Now that developers are able to run their tests with a simple Jenkins job, what should we do with that information? A CSV dump of Locust output might be helpful to some, but the vast majority of developers that merely want to validate the performance implications of their changes. So that really doesn’t really net us much gain for our general audience. We realized we needed a way to compare the performance of one run against some sort of baseline. Rather than try to transform the CSV data into a more persistent datastore, we looked to the opensource community.</p><p>Luckily, it seems we were not alone in our endeavor: the <a href="https://github.com/SvenskaSpel/locust-plugins">locust-plugins</a> project contained exactly what we needed with their <a href="https://github.com/SvenskaSpel/locust-plugins#listeners">listeners plugin</a>. Once you add the plugins package to your project, sending data to a database is as simple as <a href="https://www.postgresql.org/docs/9.1/libpq-envars.html">configuring default postgres environment variables</a> in your tasks.py [e.g. PGHOST, PGPORT, PGDATABASE, PGPASSWORD] and <a href="https://github.com/SvenskaSpel/locust-plugins/blob/953b5584dbdb3ee40feed0fe4434a02b492a53db/examples/timescale_listener_ex.py#L13">adding a listener to your tasks</a>.</p><p>I won’t get too much into the Postgres setup, since there’s no shortage of guides to getting it set up in any sort of environment you can imagine. I will say though, rather than using TimescaleDB as indicated in the plugin repo, we simply used the <a href="https://github.com/bitnami/charts/tree/master/bitnami/postgresql">official Postgresql helm chart,</a> mounting the <a href="https://github.com/SvenskaSpel/locust-plugins/blob/master/locust_plugins/timescale_schema.sql">timescale schemas</a> as an initialization script to bootstrap the DB. This was simply due to our familiarity with setup of the official Postgres chart; TimescaleDB does appear to offer <a href="https://github.com/timescale/timescaledb-kubernetes">their own charts</a>, so feel free to check those out!</p><h3>Visualizing the data</h3><p>Now that we have our run data retained in a logical format, let’s visualize it! And what better tool for that than the ever-ubiquitous Grafana? Again, I won’t get too much into deploying Grafana within your environment, since there’s a multitude of resources out there. However, I will talk about one <em>very</em> neat feature that we leveraged for our setup. We are big into configuration as code here at ReviewTrackers, and if anyone has ever spent hours setting up a dashboard before, you know you do <em>not</em> want to lose all that hard work.</p><p>The <a href="https://github.com/helm/charts/tree/master/stable/grafana">official Grafana helm chart</a> offers up instructions for <a href="https://github.com/helm/charts/tree/master/stable/grafana#import-dashboards">how to import dashboards</a>, but I still required a fair bit of tweaking in order to get it working properly. The main gotcha we found was that if you want to configure a dashboard, you must also configure a matching dashboardProvider. As illustrated below, in order for our load-test dashboardConfigMap to be properly loaded, you need to first configure a load-test provider, <em>and</em> your options.path for said provider must match the name as well.</p><pre>dashboardProviders:</pre><pre>  dashboardproviders.yaml:</pre><pre>    apiVersion: 1</pre><pre>    providers:</pre><pre>    - name: &#39;load-test&#39;</pre><pre>      orgId: 1</pre><pre>      folder: &#39;Load Test&#39;</pre><pre>      type: file</pre><pre>      disableDeletion: true</pre><pre>      editable: true</pre><pre>      options:</pre><pre>        path: /var/lib/grafana/dashboards/load-test</pre><pre>dashboardsConfigMaps:</pre><pre>  load-test: grafana-dashboard-locust</pre><p>Our dashboard is based largely on the excellent one provided by <a href="https://grafana.com/grafana/dashboards/10878">cyberw on GrafanaLabs</a>. As you might be able to tell above, we track our dashboard.json via our own source control, and import it as a configmap into our cluster. If you’re a savant, you can technically edit your dashboard directly in this json, but we found it much easier to edit dashboards directly in Grafana, export them as json, and use that to update source control.</p><p>Now that the chart was ready (and <a href="https://github.com/helm/charts/blob/871a1bed72bcd357e253bbe0d3cb92438304f42d/stable/grafana/values.yaml#L321">we configured our Postgres data source</a>), we deployed it, and after navigating to the load-test dashboard in Grafana, things were starting to come together:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OGhxmVbm5IyGJdyqIbUrMQ.png" /><figcaption>If you don’t have data yet, your dashboard will look very empty and potentially throw errors</figcaption></figure><p>If you’ve already sent some run data to your Postgres database, simply find the time range for your tests, and you should see your data! If not, kick one off and watch the data pour in! Once you have data, you can filter by endpoint by clicking it in the dropdown at the top, or clicking the name within an individual graph.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2b-ptWkr4Gl4C7haFx7Tgg.png" /><figcaption>Now we’re visualizing!</figcaption></figure><h3>Providing meaningful run data</h3><p>Now we have our beautiful dashboard for people to view locust data, but they still can’t really extrapolate much from it. Say for example there were multiple runs at the same time, or and extended period of time where no tests were run: it’s going to make it hard to isolate your data, or compare it to previous runs if there’s a large time break. In order to work around this, we need to:</p><ol><li>Identify each run as an individual test</li><li>Provide a way to easily compare your test to previous runs</li></ol><p>Luckily, Locust provides a very easy way to achieve the first goal - within the listener plugin, you can pass the testplan variable to uniquely identify each individual test. We simply set this to an environment variable based on the Jenkins build ID, and voila, the dropdown in the dashboard allows you to choose individual runs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*SLrDdJiXl81RGTZxPpkAZA.gif" /><figcaption>Bonus points if you timestamp your Jenkins runs to provide links to accurate time ranges in run results</figcaption></figure><p>Now that we were able to identify each run, we wanted a way to compare against some sort of baseline. Rather than use an aggregate of individuals’ runs (which may be subject to a lot of outliers due to failing tests), we decided to aggregate the data from our runs against staging. Since staging is our closest environment to production - and we regularly run the same tests against staging to test capacity - we figured the best way to achieve this was to place the averages from staging next to the run’s results directly on the dashboard.</p><p>Now as great as Grafana is, can be kind of a pain. If we wanted to do something like join two queries, the result set wasn’t always as pretty as we’d like it to be. Luckily, just before we started our visualization endeavor, they released Grafana 7, which includes a neat little feature known as <a href="https://grafana.com/blog/2020/06/11/new-in-grafana-7.0-data-transformations-for-all-visualizations-that-support-queries/">data transformations</a>. Essentially what these do is allow you to chain all sorts of result filters together, such as, let’s say, joining two result sets, and re-organizing the columns in a more readable way ;)</p><p>We added the following query to the table:</p><pre>SELECT Req1.name, Req1.request_type as &quot;request type&quot;, <br>  AVG(Req1.response_time) as &quot;Average - STAGING&quot;,<br>  percentile_cont(0.95) within group (order by Req1.response_time)   as &quot;95 perc - STAGING&quot;, <br>  percentile_cont(0.99) within group (order by Req1.response_time) as &quot;99 perc - STAGING&quot;, <br>  max(Req1.response_time) as &quot;max - STAGING&quot;<br>FROM request Req1<br>WHERE Req1.testplan =&#39;staging&#39;<br>GROUP BY  Req1.request_type, Req1.name</pre><p>And then simply went over to the ‘Transform’ tab, joined on the proper column (name of test), and sorted the columns how we wanted them:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/797/1*q8EnJA7sv5-Pj4SbFxqk2g.png" /><figcaption>Grafana transformations are wonderful</figcaption></figure><p>An just like that, each column has it’s respective staging average right next to it! There’s more cool stuff you can do like changing colors over certain result thresholds. Definitely play around with transformations, they can make your dashboards even more powerful!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YcnKQOeu_kK9Qdt8ly8v5A.png" /><figcaption>Individual run data right next to staging averages</figcaption></figure><h3>Putting it all together</h3><p>By establishing an accessible method for developers to run load tests (ChatOps ftw), we are able to provide our engineers with a quick, reliable way to test the performance impact of their changes. With pretty basic deployments of Grafana and Postgres, as well as adding the Locust plugins package, we can retain our run data in a much more usable manner. Finally, by leveraging the new Grafana data transformations functionality, we are able to provide a simple dashboard to our engineers so that they can quickly determine the impact of their changes.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=da627036c2c6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/visualizing-load-test-data-with-locust-and-grafana-da627036c2c6">Visualizing Load Test Data with Locust and Grafana</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Our Load Testing Journey with Locust]]></title>
            <link>https://medium.com/reviewtrackers-engineering/our-load-testing-journey-with-locust-c599670bc41b?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/c599670bc41b</guid>
            <dc:creator><![CDATA[zach attas]]></dc:creator>
            <pubDate>Mon, 17 Aug 2020 17:29:26 GMT</pubDate>
            <atom:updated>2020-08-17T21:38:31.457Z</atom:updated>
            <content:encoded><![CDATA[<h3>Our Load Testing Journey With Locust</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mn8XG5jzHTFJc41xRPNXrA.gif" /><figcaption>Real-time RPS Locust graph</figcaption></figure><p>Some organizations start Load Testing with a specific goal, like this:</p><blockquote>During Black Friday, we need to support 100k users checking out of our site</blockquote><p>At ReviewTrackers our specific goal around Load Testing was to provide evidence that we could support 3x peak user traffic. However, we also had some less quantifiable engineering goals, the kind of goals that make you feel <em>warm and fuzzy inside</em>. The ones that you imagine are used at the most cutting-edge techie companies. These goals were to have:</p><ul><li>A baseline performance for our APIs, to be able to determine if they had become slower <strong>as we did development</strong>. We’d have a check in CI that would fail if any API regressed.</li><li>An easy way to elicit bottlenecks or outliers with our APIs at high load, or when dealing with lots of data. The tests would be able to provide consistent proof of this. Then, we could use the tests to benchmark any improvements we made to those kinds of requests.</li></ul><p>There were some other requirements of our tests:</p><ul><li>Tests need to be able to run in CI, and also, against deployed environments. This is important because of the differences in hardware between environments.</li><li>Tests need to grab data from their environment to construct the urls to hit. Many of our REST APIs require specific information to work, like <em>sessions</em>, <em>review_ids</em>, <em>location_ids</em>, etc.</li><li>Tests need to be easy to use and contribute to. We want the engineers to contribute to them as they see fit and drive the adoption effort.</li></ul><p>We searched the internet for Load Testing tools, and settled on<a href="https://locust.io/"> locust.io</a>, a great, open source load testing tool that reminded us of Flask, a python micro framework. Both libraries are small, unopinionated, and easy to use.</p><p>However, we needed to write code on top of locust to fit it into our ecosystem. I’ll mention some of the key things we did, that weren’t so intuitive:</p><h3>#1 Use the same session, user, and account across tests</h3><p>Our api requires a session. Each login to our website generates one of these. If you login on a different computer, you get a different session, even if you are the same user.</p><p>In the real world, 3x peak user traffic is made up of various accounts, users, and sessions. However, for our purposes, it was easiest to use the same session, user, and account for each spawned <a href="https://docs.locust.io/en/stable/writing-a-locustfile.html#user-class">locust User</a>. There are a couple of reasons why we decided to do it this way and not simulate the real world:</p><ul><li>If we pointed to existing accounts/users, there’s a possibility we’d unintentionally invalidate their sessions.</li><li>We don’t want to create tons of accounts/users specifically for this testing. Ideally, we’d just target an existing large account.</li><li>The differences in data between accounts/users could add variance to our results. We’d prefer to just point to one big account, to lessen the variability.</li><li>We have a session limit per user, and we didn’t want to keep track of this number throughout the tests.</li></ul><p>Obviously, every piece of software is different, so our approach might not work for your software, if you have rate-limiting or other limitations. But this paradigm works for us. And because we are targeting one account/user, we can ensure that the account has all their features turned on, and has the ideal data to test against.</p><h3>#2 Important setup code lives outside of the locust hooks</h3><p>As discussed in #1, we need a single ReviewTrackers session and user to be passed to each spawned locust User. While we’re at it, the test should pass each locust User <em>location_ids, review_ids</em>, and other account specific information we need to cobble together urls.</p><p>However, we need the setup code to run <em>only once</em>. We want to create only one session used by all spawned locust Users. As well, the setup code goes out to the databases in question, and grabs the ids, we don’t need to do that repeatedly.</p><p>All I could find in locust’s documentation was the <a href="https://docs.locust.io/en/stable/writing-a-locustfile.html#on-start-and-on-stop-methods">on_start hook</a> that runs when spawning a new locust User. That happens too often. So a good place to put this setup code was in the body of the TaskSet. By doing it this way, setup is run once, and we can pass information to each locust User through the <em>on_start</em> hook using class variables:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d9490dd14cc9a7c7e164a9b27a88e78d/href">https://medium.com/media/d9490dd14cc9a7c7e164a9b27a88e78d/href</a></iframe><h3>#3 Override the name parameter so requests with dynamic data are grouped together</h3><p>As you can see from #2, we have dynamic urls based on data from the environment. We also add a <a href="https://en.wikipedia.org/wiki/Cryptographic_nonce">nonce</a> to every request, to ensure our requests aren’t cached by our service providers. This means every single url is unique.</p><p>By default locust groups requests by url, so without customization, our locust report was hard to use. However, we found this part of locust’s documentation talking about <a href="https://docs.locust.io/en/stable/writing-a-locustfile.html#grouping-requests-to-urls-with-dynamic-parameters">grouping requests with the name parameter</a>. Once we implemented this, our reports were much easier to read.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*47q8g9JmCq4BMhR4Zv7Siw.png" /></figure><h3>#4 Make your locust tests configurable and easy to use</h3><p>We wanted anyone to be able to jump in and easily run locust tests, without needing to know the details of how they worked. To do that, we did a couple of things:</p><ul><li>We used <a href="https://docs.python.org/3/library/venv.html">virtualenv</a> so anyone could download the packages they needed for locust, and not interfere with the python on their machines</li><li>We created an <a href="http://www.pyinvoke.org/">invoke task</a> that runs the tests with good default options</li></ul><p>We provided some environment variable arguments that users could pass in to customize their experience as well. Users could pass in:</p><ul><li>ENV: Environment to point to</li><li>ACCOUNT_ID: Account ID to use</li><li>HEADLESS: If true, tests are immediately kicked off headlessly (like they are in CI). If false, you must visit locust GUI (at <a href="http://localhost:8089/">http://localhost:8089</a>), provide RUNTIME/NUM_USERS/HATCH_RATE, and kick off the tests in GUI</li><li>RUNTIME, NUM_USERS, HATCH_RATE: defaults are RUNTIME=5m, NUM_USERS=200, HATCH_RATE=5. These arguments are necessary if HEADLESS=true. Here is the guide for <a href="https://docs.locust.io/en/stable/running-locust-without-web-ui.html?highlight=run#running-locust-without-the-web-ui">running locust tests without the web ui</a></li><li>FILTERS: Comma-delimited list of regex expressions. If passed in, only urls that match the expressions will be run. Useful for testing adding new urls</li></ul><p>Here’s what our invoke script looked like:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/12c23765fb498a645659ae56f3b85188/href">https://medium.com/media/12c23765fb498a645659ae56f3b85188/href</a></iframe><h3>Outstanding questions</h3><p>I want to end this article with some outstanding questions I had about load testing. I wasn’t able to get any answers from load testing articles or books. I hope people reading this article can offer up answers to these questions!</p><ul><li>Should you ping endpoints with the same arguments or different arguments?</li><li>Can you locust tests POSTs, or should you only test GETs?</li><li>Does load testing measure website experience, or just lower level performance? Is there a way to get load testing to capture async requests on a page?</li><li>Is it good design to POST a resource then GET it right away, replicating a real workflow? (Haven’t seen ANYBODY do this kind of test in example code)</li><li>Do you make ASSERTIONS on status codes, and fail load test runs based on that?</li><li>Do you make ASSERTIONS on the SPEED of endpoints? Or is load testing always for manual review?</li></ul><p>Thanks for reading this article, and I hope these locust tips are helpful!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c599670bc41b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/our-load-testing-journey-with-locust-c599670bc41b">Our Load Testing Journey with Locust</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kubernetes In-Cluster Docker Image Builds]]></title>
            <link>https://medium.com/reviewtrackers-engineering/kubernetes-in-cluster-docker-image-builds-3ae216804815?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/3ae216804815</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[David Kirk]]></dc:creator>
            <pubDate>Tue, 23 Jun 2020 19:42:06 GMT</pubDate>
            <atom:updated>2020-06-23T19:42:06.207Z</atom:updated>
            <content:encoded><![CDATA[<p>Late last year, we began the process of migrating our internal development infrastructure off Docker Swarm and onto Kubernetes. After a lot of hair pulling, tears, and struggle, we got through most of the rough patches, and onto some of the more interesting and fun parts of this project! One significant challenge early on was to figure out a good way of building docker images, which is what we’ll be talking about today. It took us a few tries, but we think we’ve landed on a pretty solid implementation that’s worth sharing!</p><p>Each deployment of our product is running across 30ish docker containers, so having to build it all from scratch takes a <em>long </em>time —unacceptably long for a stage in a core development process<em>. </em>We need some solution that allows us to reuse the docker cache for every build — regardless of where it is taking place in the cluster. Additionally, we needed that solution to be straight forward to deploy and run in Kubernetes.</p><p>We’ll briefly cover some of the earlier attempts at solving this issue, then go over the solution that we landed on and are using in production right now!</p><h3>Early failures — Kaniko and DinD Sidecar</h3><p>We initially wanted to go with Kaniko, a tool created by Google that’s intended for building docker images in a Kubernetes cluster. While this sounded <em>perfect</em> for doing the job, we quickly ran into issues. First, each build is run in a separate container, which sounds like a good idea, but that also means that each individual build has a non-negligible startup time — especially if it needs to spin up a new VM to deploy to! Beyond that, we ran into consistent issues with getting a build cache to work — I won’t go into detail, but the documentation for how to use Kaniko with Skaffold is almost non-existent, so that plan had to be shelved.</p><p>We decided to bake our own solution, and, after a screening of Inception, we came across the idea of using a Docker-in-Docker container to do our builds. Outside of lame DevOps jokes, it can be hard to find a good use for Docker-in-Docker, but this scenario actually ends up being pretty appropriate! This solution allows us to treat our image building service like any other Kubernetes deployment, while also isolating those builds away from the rest of the cluster.</p><p>Our first attempt at this involved augmenting our Jenkins agent pods with DinD sidecars. While this did technically work, we ran into very significant issues regarding, again, the build cache. Initially, we were hoping to just be able to download base images to our builders’ docker caches, but that took <em>way</em> too long. Additional experiments with prepopulating a cache &amp; copying it to other containers also ended up either not working, or being too slow. We felt like we were really onto something, but the final pieces weren’t really clicking in place.</p><h3>The Final Pieces Clicking In Place</h3><p>After a few more struggles, it became clear that we needed to rethink our strategy. We were already consuming the docker daemon endpoint from another container, so what if we stop restricting it to being just a sidecar, and instead expose it as an endpoint that can be hit from anywhere in the cluster?</p><p>We decided to spin up a DinD pod on a relatively beefy machine (c5.2xlarge), and dedicate that whole machine to docker builds. Whenever a Jenkins job calls for building an image, it runs on a “docker-builder” agent, whose DOCKER_HOST targets the DinD service. If a job calls for running a container, it instead runs on a “docker-runner” which has its own small DinD sidecar!</p><p>This allows us to share the docker cache across <em>every</em> build in our cluster, which has led to way more consistency in our builds, and greatly simplified the process of building a container. Our worst-case build time happens far less often. While there are some negatives, such as not benefitting from autoscaling, the positives in terms of simplicity to use and maintain make us believe that this is a solid solution that we plan on sticking with for a long time.</p><p>Our DinD Service is configured with the following env vars to enable this behavior:</p><pre>spec:<br>  containers:<br>  - env:<br>    - name: DOCKER_TLS_CERTDIR<br>      value: [PATH TO TLS CERT DIR]</pre><p>And our image builders contain the following env var (the DinD service is the jenkins namespace)</p><pre>spec:<br>  containers:<br>  - env:<br>    - name: DOCKER_HOST<br>      value: tcp://dind.jenkins:2375</pre><p>And that’s about it! We’ve been using this solution in our CI cluster for a few months now, and the only problem we’ve seen so far is disk space usage, which can be managed with just a few docker image cleanup commands. Overall, we’re very happy with how well this has performed, and hopefully it’ll treat you just as well!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3ae216804815" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/kubernetes-in-cluster-docker-image-builds-3ae216804815">Kubernetes In-Cluster Docker Image Builds</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Refactoring slow code using instrumentation]]></title>
            <link>https://medium.com/reviewtrackers-engineering/refactoring-slow-code-using-instrumentation-ea9a4e798b75?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/ea9a4e798b75</guid>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[scaling]]></category>
            <category><![CDATA[instrumentation]]></category>
            <dc:creator><![CDATA[Andrew Platkin]]></dc:creator>
            <pubDate>Mon, 08 Jun 2020 13:13:44 GMT</pubDate>
            <atom:updated>2020-06-08T18:35:25.850Z</atom:updated>
            <content:encoded><![CDATA[<p>A few weeks ago, we started seeing a series of timed out requests against our Amplify service in production. This service is responsible for supporting our review widgets that people embed on their own site. I’ve done some posts in the past on Amplify with <a href="https://engineering.reviewtrackers.com/taming-our-postgres-io-problem-with-a-materialized-view-57d19a21e032">Taming our Postgres IO Problem with a Materialized View</a> and <a href="https://engineering.reviewtrackers.com/custom-built-request-caching-in-ruby-7df5142636ee">Custom built request caching in Ruby</a> that highlight some of the design decisions made to ensure that the service is very fast and can handle a bunch of concurrent requests.</p><p>I noticed that our traffic for this service had steadily increased over the last few months, which was now approaching about 2.5M monthly views. Going from ~160k weekly views in January to about ~575k weekly views in May or a 250% increase in traffic (go us!).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uqVDphP_K04uTrMZf3wsWA.png" /><figcaption>Increase in Amplify Usage</figcaption></figure><h3>Investigating the problem</h3><p>So my hypothesis was that one of our slower endpoints was eating up to much HTTP traffic, which was then slowing down all endpoints at peak usage time. I went into New Relic (the tool we use for instrumentation) to investigate. Sure enough, it looks like one of our endpoints was accounting for the bulk of the HTTP traffic</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bWSNTCWmFHlHmgirPPbC-Q.png" /><figcaption>New Relic Transaction Details</figcaption></figure><p>The analytics endpoint is responsible for passing analytic events like “views” to our analytics provider is taking about <strong>75ms </strong>on average per request. For comparison, most of our widget endpoints take about <strong>10ms</strong> on average. Additionally, it takes up about <strong>67% </strong>of our HTTP traffic for the service, which is really high. Since this endpoint is invoked every time a widget is loaded on a page, this endpoint being slow can start to affect other endpoints with an increase in traffic to Amplify due to only so much concurrent HTTP capacity.</p><p>So to ensure we’re able to better scale to this increase in traffic, we need to address the slowness of this endpoint, which would then ensure that we can support more concurrent requests.</p><p><strong>Short Term</strong></p><p>Since we use Heroku to host our applications, scale up the number of web dynos (servers shared within their load balancer for the app) for the service, which will increase the number of concurrent requests it can support for the time being. This will compensate for the slowness in this one endpoint.</p><p><strong>Long Term</strong></p><p>Identify the bottlenecks in the endpoint and refactor it.</p><h3>Digging into the endpoint</h3><p>New Relic provides a way to see the breakdown of the endpoint’s time to see where the bottleneck could be. When looking at the breakdown table, it looks like most of the time is coming from the PUT to RabbitMQ. Basically we use RabbitMQ internally as a message broker for Amplify. A request is made to POST the analytics payload to our API, which then queues a message to the relevant RabbitMQ queue on our exchange. Then an analytics consumer takes the payload and interacts with the analytics system to pass along the information.</p><p>The goal of this architecture is to prevent situations where the analytics system slows down our API. As long as we can ensure that our interaction with our RabbitMQ server is fast, this request should be pretty quick. Typically publishing a message to RabbitMQ should be a really fast operation, so it seemed odd that it was accounting for most of the time.</p><p>After looking at the code, I came to the conclusion that the reason it was taking so long was because we were creating a new connection and channel to RabbitMQ every request and then waiting to close the connection. The Publisher service not pictured below creates a connection and channel as part of being instantiated.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7aa24d923edefa3a3bd4600c169a24d4/href">https://medium.com/media/7aa24d923edefa3a3bd4600c169a24d4/href</a></iframe><h3>Refactoring the endpoint</h3><p>Creating the connection/channel and closing it every request seemed pretty wasteful to me, since we were incurring the overhead every request. One of the complexities however is that we use <a href="https://github.com/puma/puma">Puma</a> as web server, which gives us additional concurrency for our API using threading. Since threads are being used, we need to ensure it’s thread-safe in that the connection/channel can be shared amongst many threads spawned by a Puma process. Additionally, I noticed that overall we have pretty low memory usage for our Web dynos. This made me think we could also leverage a feature of Puma called <a href="https://github.com/puma/puma#clustered-mode">clustered mode</a>. Clustered mode allows Puma to fork the main process into multiple child processes called “workers”, which then each child process spawns threads (in this case we were spawning 16 threads). If we have two workers that each have 16 threads, that means that we can support 32 concurrent requests on a single Web dyno.</p><p>So to ensure that the operations are thread-safe, I navigated to Bunny’s documentation (the Ruby gem we used to interact with RabbitMQ). It highlighted that we should create a global connection pool for each <a href="http://rubybunny.info/articles/connecting.html#using_bunny_with_unicorn">forked process</a>. Thankfully, Puma makes this pretty easy with on_worker_boot and on_worker_shutdown blocks that it provides as part of its configuration.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a66a094a11678f036f88781a7141791e/href">https://medium.com/media/a66a094a11678f036f88781a7141791e/href</a></iframe><p>Then with this new globally available <strong>$rabbitmq_channel, </strong>I can update my publisher to accept it as an argument instead of creating its own connection/channel and having to close it</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e7eb2dc7463e4f674b6ee2da7aa25861/href">https://medium.com/media/e7eb2dc7463e4f674b6ee2da7aa25861/href</a></iframe><p>With the new Puma configuration, I just need to set MAX_CONCURRENCY to 2 as an environment variable, so that I can fork my process into 2 instead of 1. This will eat up more memory, but we have a ton to spare since not much is used in production.</p><p>To understand if my changes made an impact with the endpoint, I re-used my benchmark script using <a href="https://github.com/tsenart/vegeta">Vegeta</a> to see its performance at 500 requests/s for 10 seconds against my service locally.</p><p><strong>Before</strong></p><pre>-- Request Info --<br><br>Num Requests: 5000<br>Request Duration: 9.998184723s<br><br>-- Metrics --<br><br>Mean: 313.888607ms<br>50th percentile: 0s<br>95th percentile: 99.389346ms<br>99th percentile: 11.099165562s</pre><p><strong>After</strong></p><pre>-- Request Info --<br><br>Num Requests: 5000<br>Request Duration: 9.998289959s<br><br>-- Metrics --<br><br>Mean: 45.002096ms<br>50th percentile: 2.728643ms<br>95th percentile: 88.35957ms<br>99th percentile: 1.167418792s</pre><p>As you can see, the average response time went down considerably with these changes from <strong>313ms</strong> to <strong>45ms</strong>, an <strong>86%</strong> improvement.</p><h3>Measuring the impact in Production</h3><p>So to understand if my changes had a positive effect on production as they did on my local, I looked at New Relic again after it was deployed for a few hours.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MaxEzwKRhUftjy--dP25TA.png" /><figcaption>New Relic After</figcaption></figure><p>As you can see our average response time for the endpoint went down from <strong>75.3ms</strong> to <strong>12.5ms </strong>in production. Also, the endpoint now only accounts for <strong>37.9% </strong>of our HTTP traffic. With the two Web dynos running and the forked processes, we are now able to support 64 concurrent requests, which means that we can handle 4 times the amount of traffic than where we were when we started. Hopefully, this will give us more runway for the next few months.</p><h3>Key Takeaways</h3><p><strong>Instrumentation</strong></p><p>Setting up instrumentation is super important when deploying a new service. As things like this come up, it’s very convenient to be able to understand what endpoints are slow and what parts of the request make up the bulk of that time. This makes fixing issues so much faster since most of the exploratory work is done for you in Production.</p><p><strong>Proactively fix performance issues</strong></p><p>As your API gets more usage, it’s important to keep an eye out for bottlenecks and prioritize fixes. This will ensure that slow endpoints don’t eat up all of the request capacity, which will then affect the performance of your API as a whole.</p><p><strong>Benchmark performance issues locally</strong></p><p>When testing a performance fix, it’s important to have realistic benchmarks for before/after performance so that your change has a high probability of actually working in production. I typically download a copy of the production database locally when testing, so that I can work with realistic benchmarks. For this specific fix, it wasn’t anything database related, but for others it may be. Using tools like <a href="https://github.com/tsenart/vegeta">Vegeta</a> also help to give you a clear picture on the effects your change has made to the performance of the API at production scale.</p><p>If you have any questions about our approach or just want to talk shop, you can reach me on github at <a href="http://github.com/plattyp">http://github.com/plattyp</a> and email <a href="/service/mailto:andrew@reviewtrackers.com">andrew@reviewtrackers.com</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea9a4e798b75" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/refactoring-slow-code-using-instrumentation-ea9a4e798b75">Refactoring slow code using instrumentation</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Our journey of integrating GraphQL into our REST app — learning to swim]]></title>
            <link>https://medium.com/reviewtrackers-engineering/our-journey-of-integrating-graphql-into-our-rest-app-learning-to-swim-d171cbad71c6?source=rss----62b16e39f4f---4</link>
            <guid isPermaLink="false">https://medium.com/p/d171cbad71c6</guid>
            <category><![CDATA[rest-api]]></category>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[react-native]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Yohei Kanehara]]></dc:creator>
            <pubDate>Fri, 06 Dec 2019 15:39:11 GMT</pubDate>
            <atom:updated>2019-12-06T15:40:12.718Z</atom:updated>
            <content:encoded><![CDATA[<h3>Our journey of integrating GraphQL into our REST app — learning to swim</h3><p><em>TL;DR GraphQL is amazing and can be incrementally integrated into a REST app but be wary of the client side cache.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*di7bhLC_gIwXRtEBiOvwSg.png" /></figure><p>The story of GraphQL at ReviewTrackers goes all the way back to our very <a href="https://engineering.reviewtrackers.com/reviewtrackers-hackathon-part-2-hacking-together-a-graphql-server-cab99f9ef305">first Hackathon</a>. What started out as an experimental “hack” project has matured into a production service that now serves our most important page in our application — the reviews page. While plenty of articles and tutorials exist on integrating GraphQL into greenfield applications, there is a lack of resources on integrating GraphQL into existing applications. I’d like to share our journey of integrating GraphQL into our app, both the ups and and downs, so hopefully others can take from our experience when considering to adopt GraphQL. I compare our experience to swimming — incrementally dipping our feet into the pool until we jumped into the deep end…</p><h3>Background: Why GraphQL?</h3><p>At ReviewTrackers, we maintain two applications — a React web application and a React Native mobile application. Both applications interface with our backend through a REST API. As our app grew and our data models became more complex, our client codebases also grew in complexity as it had to fetch and update all this complex data. A lot of our data is also relational — e.g. a review belongs to a location which in turn belongs to an account. This inevitably lead to <a href="https://stackoverflow.com/questions/44564905/what-is-over-fetching-or-under-fetching">over fetching / under fetching</a> issues on our front end which we initially solved with sql views and dedicated REST endpoints for complex, relational data. While this was performant and worked, it was a clunky, time consuming dev experience and didn’t scale well since we’d have to deploy database migrations each time we needed new data and would also tightly couple our client applications to our database model. Not to mention that we’d have to make changes to both our web app and mobile app code bases if there were any breaking changes in the REST API. In short, we found that modern front end applications require a lot of flexibility in how they declare their data requirements — the exact pain point that GraphQL was designed to solve. After a <a href="https://engineering.reviewtrackers.com/reviewtrackers-hackathon-part-2-hacking-together-a-graphql-server-cab99f9ef305">successful hackathon</a> of proving GraphQL’s viability, we started to slowly integrate GraphQL into our application.</p><h3>Incremental adoption</h3><p>At ReviewTrackers, we practice continuous deployment and push to production multiple times a day. This means that any change to our system should be incrementally deployed and not block our deployment pipeline — each deployment should be de-risked and other devs should be able to deploy. This means that a complete rewrite from REST to GraphQL was out of the question. Rewrites in my opinion are also <a href="https://daedtech.com/the-myth-of-the-software-rewrite/">rarely the right solution</a>. So we strategically wanted to integrate GraphQL into our app through a path of least resistance — slowly dipping into the pool. We did this by keeping our interface to our backend consistent by wrapping our REST api in a GraphQL layer rather than interfacing with our database directly. In terms of integrating GraphQL into our client applications, we identified data fetching as the natural first step.</p><h3>Data fetching: getting our feet wet</h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2FK6Q7ZCdLy8pCE%2Ftwitter%2Fiframe&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2FK6Q7ZCdLy8pCE%2Fgiphy-downsized.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2FK6Q7ZCdLy8pCE%2Fgiphy.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="435" frameborder="0" scrolling="no"><a href="https://medium.com/media/2a000eac79c5a8057e92736829fd302b/href">https://medium.com/media/2a000eac79c5a8057e92736829fd302b/href</a></iframe><p>Most of our existing components fetched data and subscribed to them through redux like so:</p><pre>const LocationName = ({location, loading, error, fetchLocation, locationID}) =&gt; {  <br>  useEffect(() =&gt; fetchLocation(locationID), [])<br>  if (loading) return &lt;p&gt;Loading...&lt;/p&gt;<br>  if (error) return &lt;p&gt;An error occurred&lt;/p&gt;</pre><pre>  return &lt;h1&gt;{location.name}&lt;/h1&gt;<br>}</pre><pre>connect(<br>  state =&gt; ({ <br>    location: state.resources.location, <br>    error: state.resources.errors.location, <br>    loading: state.resources.loading.location  <br>  }),<br>  dispatch =&gt; ({ <br>    fetchLocation: (id) =&gt; dispatch(fetchLocationAction(id)) <br>  })<br>)(LocationName)</pre><p>This seems like a really nice component since everything is encapsulated into this one component but this type of component can introduce over fetching problems. For example, if we were rendering the component in two parts of the screen, we would be redundantly fetching the same location over the network. To mitigate, we could lift this fetching up to a higher common ancestor. Over time, this strategy made our root container components unruly with multiple resources being fetched, various loading and error states managed in redux, etc. Not to mention that our original component would become disjointed and dependent on another component.</p><blockquote>In short, our components no longer became encapsulated.</blockquote><p>Client side GraphQL libraries like Apollo solve this through a cache layer to prevent over fetching. So in the same scenario where our component is rendered in two parts of the screen, there will only be one trip over the network to retrieve the location which will then be cached and reused when another request for the location is fired. This allows us to build the encapsulated version of our component like so:</p><pre>const GET_LOCATION_NAME = gql`<br>  query GetLocationName($locationID: ID!) {<br>    location(id: $locationID) { <br>      name <br>    }<br>  }<br>`</pre><pre>const LocationName = ({locationID}) =&gt; {  <br>  const {data, error, loading} = useQuery(<br>    GET_LOCATION_NAME, { variables: { locationID } }<br>  )<br>  if (loading) return &lt;p&gt;Loading...&lt;/p&gt;<br>  if (error) return &lt;p&gt;An error occurred&lt;/p&gt;</pre><pre>return &lt;h1&gt;{location.name}&lt;/h1&gt;<br>}</pre><p>Personally, I love this style of building data rich components. The loading and error states along with the data fetched are all encapsulated in a react hook. As long as we kept our data shape the same, we could migrate our redux / REST based components to GraphQL — amazing!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*gw-ijoG3OP0K28_rm8r95A.gif" /><figcaption>The GraphQL hype is real</figcaption></figure><blockquote>While we were stoked, the client side cache actually ended up biting us in more nuanced scenarios…</blockquote><h3>The reviews page: going swimming</h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2Fr6ALgGVKLg93O%2Ftwitter%2Fiframe&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2Fr6ALgGVKLg93O%2Fgiphy.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2Fr6ALgGVKLg93O%2F200.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="290" frameborder="0" scrolling="no"><a href="https://medium.com/media/e7ed71ced55aafb67ab9568d77b31cd4/href">https://medium.com/media/e7ed71ced55aafb67ab9568d77b31cd4/href</a></iframe><p>We continued getting our feet wet with GraphQL by migrating less risky components to GraphQL. Everything was going smoothly when a bigger use case for GraphQL arose when we decided to refactor our reviews page to be more performant.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uo1cw4C46WuygY0Pp3RF7A.png" /><figcaption>The reviews page</figcaption></figure><p>As a review tracking application, the reviews page is crucial to our users. As our company grew, so did our data which lead to performance issues for accounts with large review volumes. We tried <a href="https://engineering.reviewtrackers.com/speeding-up-our-api-with-redis-a47736278511">tuning it with Redis</a> in the past but eventually realized we would need to implement a <a href="https://engineering.reviewtrackers.com/the-100-million-row-dash-9e888539df51">new cursor based API</a> to reap the biggest performance boost.</p><p>Cursor based pagination is <a href="https://graphql.org/learn/pagination/#pagination-and-edges">the recommended way to paginate in GraphQL</a> and we knew we’d have to implement this new API in both our web app and mobile app so we took this as an opportunity to implement this in GraphQL. It was the perfect fit!</p><p>After a few weeks of hard work, we rolled out the first version of the new reviews page (web only) behind a feature flag to a small subset of our customers. The results were tremendous with our largest accounts loading <strong>~1900% faster</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*ZADuUgFrSeGjptISugGt6Q.gif" /><figcaption>Side by side comparison (5x speed)</figcaption></figure><p>It wasn’t without some hiccups though…</p><h3>The client side cache: jumping into the deep end</h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2FieZe796AFlVMk%2Ftwitter%2Fiframe&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2FieZe796AFlVMk%2Fgiphy-downsized.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2FieZe796AFlVMk%2F200.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="238" frameborder="0" scrolling="no"><a href="https://medium.com/media/7f99ea4332318b2f75a8f711c4e9653f/href">https://medium.com/media/7f99ea4332318b2f75a8f711c4e9653f/href</a></iframe><p>Remember that client side cache? It was amazing and worked so well that some users complained the page was unresponsive when changing filters — a result of cached reviews causing the page to load too fast. We were stoked about how well this worked until we noticed some weird things…</p><p>When users would update a review status in the review detail page:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5Y8jHq4W8YAAlg4EwSzCpg.png" /></figure><p>They noticed it would not reflect on the reviews page without a page refresh:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mwBb5kM5vu1Ib2o_8gzhlA.png" /></figure><p>It turns out that our GraphQL cache is not updated when the POST request to update the review status is fired, causing this stale cache issue.</p><p>Sad 😞</p><p>To fix this, we updated the review status dropdown to also be driven by GraphQL so the client cache would be updated. This worked and we were able to still reap the benefits of the client side cache 😎.</p><p>Until we realized the same issue occurred when adding tags to reviews…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Yzj85lA2g-pU_CqAHN6akA.png" /></figure><p>And when responding to reviews…</p><p>And basically any REST mutation not going through our GraphQL layer…</p><p>Sad 😞</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2FEYmAHLpw5LBbG%2Ftwitter%2Fiframe&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2FEYmAHLpw5LBbG%2F200w_d.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2FEYmAHLpw5LBbG%2Fgiphy.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="290" frameborder="0" scrolling="no"><a href="https://medium.com/media/2c5e6a92a6f2b9dd8d6e5fe4e0fd39cf/href">https://medium.com/media/2c5e6a92a6f2b9dd8d6e5fe4e0fd39cf/href</a></iframe><p>Apollo provides some ways to <a href="https://www.apollographql.com/docs/react/caching/cache-configuration/">manipulate the cache</a> directly, which we initially tried but it turned out too cumbersome and imperative that we backtracked.</p><blockquote>There are only two hard things in Computer Science: cache invalidation and naming things.</blockquote><blockquote>— Phil Karlton</blockquote><p>We ultimately ended up backtracking on the use of the cache entirely by configuring our Apollo client to use the network-only <a href="https://www.apollographql.com/docs/react/api/react-apollo/#optionsfetchpolicy">fetch policy</a> which bypasses the client side cache and fires a network request per query. While we would take a slight performance hit, this tradeoff was worth it to avoid the headaches of cache eviction. We also were only firing the query in a single root component — i.e. multiple components weren’t firing the same query like our LocationName component above. This simple change allowed us to keep our reviews page in sync with any REST mutations, fixing all the stale cache issues above.</p><h3>Configuring the GraphQL client: floatation device</h3><p>We found that configuring and tuning the GraphQL client when integrating GraphQL into a REST application is key — our floatation device 😉.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2Fetn52ENYVnpxqMaXiT%2Ftwitter%2Fiframe&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2Fetn52ENYVnpxqMaXiT%2Fgiphy-downsized.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2Fetn52ENYVnpxqMaXiT%2Fgiphy.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=giphy" width="435" height="435" frameborder="0" scrolling="no"><a href="https://medium.com/media/0e8907f601d65afa5fb1837364a90991/href">https://medium.com/media/0e8907f601d65afa5fb1837364a90991/href</a></iframe><p>We configured our Apollo client to default to a cache-and-network fetch policy which returns data from the cache if present but fires a network request in the background to validate the data and update the cache if needed (by default, the Apollo client will skip network requests entirely if data is in the cache).</p><blockquote>This fetch policy optimizes for users getting a quick response while also trying to keep cached data consistent with your server data at the cost of extra network requests.</blockquote><p>You can configure your ApolloClient to achieve this like so:</p><pre>const client = new ApolloClient({<br>  link,<br>  cache,<br>  defaultOptions: {<br>    watchQuery: {<br>      fetchPolicy: &#39;cache-and-network&#39;,<br>      errorPolicy: &#39;all&#39;<br>    },<br>  query: {<br>    fetchPolicy: &#39;cache-and-network&#39;,<br>    errorPolicy: &#39;all&#39;<br>  },<br>  mutate: {<br>    errorPolicy: &#39;all&#39;<br>  }<br>}})</pre><p>Now all our queries will fire network request to revalidate the cache to avoid any issues with mutations occurring outside our GraphQL client. Furthermore, each GraphQL query can be individually configured to use a different network policy if needed — e.g. our reviews page still uses the network-only network policy to make sure we never show stale review data.</p><p>Our newly tuned GraphQL configuration allowed us to successfully integrated GraphQL on both web and mobile clients 🎊. Overall it has been a huge success and produced amazing results barring some hiccups. I have no doubt GraphQL will continue to improve as the community and tooling around it improves.</p><h3>Key takeaways</h3><ul><li>Incrementally adopt GraphQL starting with data fetching</li><li>Have good automated tests to make sure nothing breaks along the way</li><li>Rollout GraphQL migrations incrementally to users</li><li>Configure your GraphQL client to skip the cache or revalidate the cache to avoid stale cache issues**</li></ul><p><em>** More relevant when integrating into an existing REST application</em></p><p>Questions or comments? Find me on Github or email!</p><ul><li><a href="https://github.com/kanehara">Github</a></li><li>kanehara@umich.edu</li></ul><p>Interested in solving challenging problems like this on a well funded, fun team? <a href="https://jobs.lever.co/reviewtrackers">Come work for us!</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d171cbad71c6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/reviewtrackers-engineering/our-journey-of-integrating-graphql-into-our-rest-app-learning-to-swim-d171cbad71c6">Our journey of integrating GraphQL into our REST app — learning to swim</a> was originally published in <a href="https://medium.com/reviewtrackers-engineering">ReviewTrackers Engineering, Product and Design Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>