CINXE.COM

Engineering Blog - Eventbrite Engineering

<!DOCTYPE html> <html lang="en-US" class="no-js no-svg"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="profile" href="http://gmpg.org/xfn/11"> <script>(function(html){html.className = html.className.replace(/\bno-js\b/,'js')})(document.documentElement);</script> <meta name='robots' content='index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1' /> <!-- This site is optimized with the Yoast SEO plugin v23.7 - https://yoast.com/wordpress/plugins/seo/ --> <title>Engineering Blog - Eventbrite Engineering</title> <meta name="description" content="Eventbrite Engineering" /> <link rel="canonical" href="https://www.eventbrite.com/engineering/" /> <link rel="next" href="https://www.eventbrite.com/engineering/page/2/" /> <meta property="og:locale" content="en_US" /> <meta property="og:type" content="website" /> <meta property="og:title" content="Engineering Blog" /> <meta property="og:description" content="Eventbrite Engineering" /> <meta property="og:url" content="https://www.eventbrite.com/engineering/" /> <meta property="og:site_name" content="Engineering Blog" /> <meta name="twitter:card" content="summary_large_image" /> <script type="application/ld+json" class="yoast-schema-graph">{"@context":"https://schema.org","@graph":[{"@type":"CollectionPage","@id":"https://www.eventbrite.com/engineering/","url":"https://www.eventbrite.com/engineering/","name":"Engineering Blog - Eventbrite Engineering","isPartOf":{"@id":"https://www.eventbrite.com/engineering/#website"},"description":"Eventbrite Engineering","breadcrumb":{"@id":"https://www.eventbrite.com/engineering/#breadcrumb"},"inLanguage":"en-US"},{"@type":"BreadcrumbList","@id":"https://www.eventbrite.com/engineering/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home"}]},{"@type":"WebSite","@id":"https://www.eventbrite.com/engineering/#website","url":"https://www.eventbrite.com/engineering/","name":"Engineering Blog","description":"Eventbrite Engineering","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://www.eventbrite.com/engineering/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"}]}</script> <!-- / Yoast SEO plugin. --> <link rel='dns-prefetch' href='//www.eventbrite.com' /> <link rel='dns-prefetch' href='//secure.gravatar.com' /> <link rel='dns-prefetch' href='//stats.wp.com' /> <link rel='dns-prefetch' href='//fonts.googleapis.com' /> <link rel='dns-prefetch' href='//v0.wordpress.com' /> <link href='https://fonts.gstatic.com' crossorigin rel='preconnect' /> <link rel="alternate" type="application/rss+xml" title="Engineering Blog &raquo; Feed" href="https://www.eventbrite.com/engineering/feed/" /> <link rel="alternate" type="application/rss+xml" title="Engineering Blog &raquo; Comments Feed" href="https://www.eventbrite.com/engineering/comments/feed/" /> <script type="text/javascript"> /* <![CDATA[ */ window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/15.0.3\/72x72\/","ext":".png","svgUrl":"https:\/\/s.w.org\/images\/core\/emoji\/15.0.3\/svg\/","svgExt":".svg","source":{"concatemoji":"https:\/\/www.eventbrite.com\/engineering\/wp-includes\/js\/wp-emoji-release.min.js?ver=6.6.2"}}; /*! This file is auto-generated */ !function(i,n){var o,s,e;function c(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function p(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data),r=(e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0),new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data));return t.every(function(e,t){return e===r[t]})}function u(e,t,n){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\uddfa\ud83c\uddf3","\ud83c\uddfa\u200b\ud83c\uddf3")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!n(e,"\ud83d\udc26\u200d\u2b1b","\ud83d\udc26\u200b\u2b1b")}return!1}function f(e,t,n){var r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):i.createElement("canvas"),a=r.getContext("2d",{willReadFrequently:!0}),o=(a.textBaseline="top",a.font="600 32px Arial",{});return e.forEach(function(e){o[e]=t(a,e,n)}),o}function t(e){var t=i.createElement("script");t.src=e,t.defer=!0,i.head.appendChild(t)}"undefined"!=typeof Promise&&(o="wpEmojiSettingsSupports",s=["flag","emoji"],n.supports={everything:!0,everythingExceptFlag:!0},e=new Promise(function(e){i.addEventListener("DOMContentLoaded",e,{once:!0})}),new Promise(function(t){var n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(s),u.toString(),p.toString()].join(",")+"));",r=new Blob([e],{type:"text/javascript"}),a=new Worker(URL.createObjectURL(r),{name:"wpTestEmojiSupports"});return void(a.onmessage=function(e){c(n=e.data),a.terminate(),t(n)})}catch(e){}c(n=f(s,u,p))}t(n)}).then(function(e){for(var t in e)n.supports[t]=e[t],n.supports.everything=n.supports.everything&&n.supports[t],"flag"!==t&&(n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&n.supports[t]);n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&!n.supports.flag,n.DOMReady=!1,n.readyCallback=function(){n.DOMReady=!0}}).then(function(){return e}).then(function(){var e;n.supports.everything||(n.readyCallback(),(e=n.source||{}).concatemoji?t(e.concatemoji):e.wpemoji&&e.twemoji&&(t(e.twemoji),t(e.wpemoji)))}))}((window,document),window._wpemojiSettings); /* ]]> */ </script> <style id='wp-emoji-styles-inline-css' type='text/css'> img.wp-smiley, img.emoji { display: inline !important; border: none !important; box-shadow: none !important; height: 1em !important; width: 1em !important; margin: 0 0.07em !important; vertical-align: -0.1em !important; background: none !important; padding: 0 !important; } </style> <link rel='stylesheet' id='wp-block-library-css' href='https://www.eventbrite.com/engineering/wp-includes/css/dist/block-library/style.min.css?ver=6.6.2' type='text/css' media='all' /> <style id='classic-theme-styles-inline-css' type='text/css'> /*! This file is auto-generated */ .wp-block-button__link{color:#fff;background-color:#32373c;border-radius:9999px;box-shadow:none;text-decoration:none;padding:calc(.667em + 2px) calc(1.333em + 2px);font-size:1.125em}.wp-block-file__button{background:#32373c;color:#fff;text-decoration:none} </style> <style id='global-styles-inline-css' type='text/css'> :root{--wp--preset--aspect-ratio--square: 1;--wp--preset--aspect-ratio--4-3: 4/3;--wp--preset--aspect-ratio--3-4: 3/4;--wp--preset--aspect-ratio--3-2: 3/2;--wp--preset--aspect-ratio--2-3: 2/3;--wp--preset--aspect-ratio--16-9: 16/9;--wp--preset--aspect-ratio--9-16: 9/16;--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;--wp--preset--spacing--20: 0.44rem;--wp--preset--spacing--30: 0.67rem;--wp--preset--spacing--40: 1rem;--wp--preset--spacing--50: 1.5rem;--wp--preset--spacing--60: 2.25rem;--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1);--wp--preset--shadow--crisp: 6px 6px 0px rgba(0, 0, 0, 1);}:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;} :where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;} :where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;} :root :where(.wp-block-pullquote){font-size: 1.5em;line-height: 1.6;} </style> <link rel='stylesheet' id='taxonomy-image-plugin-public-css' href='https://www.eventbrite.com/engineering/wp-content/plugins/taxonomy-images/css/style.css?ver=0.9.6' type='text/css' media='screen' /> <link rel='stylesheet' id='ppress-frontend-css' href='https://www.eventbrite.com/engineering/wp-content/plugins/wp-user-avatar/assets/css/frontend.min.css?ver=4.15.17' type='text/css' media='all' /> <link rel='stylesheet' id='ppress-flatpickr-css' href='https://www.eventbrite.com/engineering/wp-content/plugins/wp-user-avatar/assets/flatpickr/flatpickr.min.css?ver=4.15.17' type='text/css' media='all' /> <link rel='stylesheet' id='ppress-select2-css' href='https://www.eventbrite.com/engineering/wp-content/plugins/wp-user-avatar/assets/select2/select2.min.css?ver=6.6.2' type='text/css' media='all' /> <link rel='stylesheet' id='twentyseventeen-fonts-css' href='https://fonts.googleapis.com/css?family=Libre+Franklin%3A300%2C300i%2C400%2C400i%2C600%2C600i%2C800%2C800i&#038;subset=latin%2Clatin-ext' type='text/css' media='all' /> <link rel='stylesheet' id='twentyseventeen-style-css' href='https://www.eventbrite.com/engineering/wp-content/themes/eng_blog_27/style.css?ver=6.6.2' type='text/css' media='all' /> <!--[if lt IE 9]> <link rel='stylesheet' id='twentyseventeen-ie8-css' href='https://www.eventbrite.com/engineering/wp-content/themes/eng_blog_27/assets/css/ie8.css?ver=1.0' type='text/css' media='all' /> <![endif]--> <link rel='stylesheet' id='jetpack-authors-widget-css' href='https://www.eventbrite.com/engineering/wp-content/plugins/jetpack/modules/widgets/authors/style.css?ver=20161228' type='text/css' media='all' /> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-includes/js/jquery/jquery.min.js?ver=3.7.1" id="jquery-core-js"></script> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-includes/js/jquery/jquery-migrate.min.js?ver=3.4.1" id="jquery-migrate-js"></script> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-content/plugins/wp-user-avatar/assets/flatpickr/flatpickr.min.js?ver=4.15.17" id="ppress-flatpickr-js"></script> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-content/plugins/wp-user-avatar/assets/select2/select2.min.js?ver=4.15.17" id="ppress-select2-js"></script> <!--[if lt IE 9]> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-content/themes/eng_blog_27/assets/js/html5.js?ver=3.7.3" id="html5-js"></script> <![endif]--> <link rel="https://api.w.org/" href="https://www.eventbrite.com/engineering/wp-json/" /><link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://www.eventbrite.com/engineering/xmlrpc.php?rsd" /> <link rel='shortlink' href='https://wp.me/7UgOl' /> <style>img#wpstats{display:none}</style> <style id="twentyseventeen-custom-header-styles" type="text/css"> .site-title, .site-description { position: absolute; } </style> <style type="text/css" id="wp-custom-css"> .site-title { background: url(https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2019/12/eventbrite.png) left center / 32px 32px no-repeat !important; } #secondary { padding-left: 20px; } .entry-content ol, .entry-content ul { list-style-position: inside; padding-left: 20px; } </style> <!-- Code inside here is served to everything except browsers less than IE9 --> <script type="text/javascript"> window.BENTON_PROPERTIES = { 'src': 'https://ebmedia.eventbrite.com/s3-build/1245126-rc2023-04-27_16.04-e56e304/django/js/src/eb/fonts/benton.js' }; </script> <script type="text/javascript" charset="utf-8">!function(a,b){if(a.EB=a.EB||{},EB.renderFonts=function(a){for(var c,d="",e=b.createElement("style"),f=0;c=a[f];f++)d+="@font-face{font-family:'Benton Sans';font-weight:"+c.weight+";font-style:"+c.style+";src:local('Benton Sans'),local('BentonSans'),url(data:application/font-woff;base64,"+c.base64+") format('woff');}";e.styleSheet&&!e.sheet?e.styleSheet.cssText=d:e.appendChild(b.createTextNode(d)),b.getElementsByTagName("head")[0].appendChild(e),b.documentElement.className+=" font-has-loaded"},a.localStorage){var c=JSON.parse(localStorage.getItem("EB.fonts.benton.01-13-2015"));if(c)EB.renderFonts(c);else{EB.shouldRenderFonts=!0;var d=b.createElement("script");d.src=a.BENTON_PROPERTIES.src,b.getElementsByTagName("head")[0].appendChild(d),setTimeout(function(){EB.shouldRenderFonts=!1},3e3)}}}(window,document);</script> </head> <body class="home blog group-blog hfeed has-sidebar title-tagline-hidden colors-light"> <div id="page" class="site"> <a class="skip-link screen-reader-text" href="#content">Skip to content</a> <header id="masthead" class="site-header" role="banner"> <div class="custom-header"> <div class="custom-header-media"> </div> <div class="site-branding"> <div class=""> <div class="site-branding-text"> <div class="navigation-top"> <div class="wrap"> <h1 class="site-title"> <a href="https://www.eventbrite.com/engineering/" rel="home">Engineering Blog</a> </h1> <nav id="site-navigation" class="main-navigation" role="navigation" aria-label="Top Menu"> <div class="menu-toggle-wrapper"> <button class="menu-toggle" aria-controls="top-menu" aria-expanded="false"> <svg class="icon icon-bars" aria-hidden="true" role="img"> <use href="#icon-bars" xlink:href="#icon-bars"></use> </svg><svg class="icon icon-close" aria-hidden="true" role="img"> <use href="#icon-close" xlink:href="#icon-close"></use> </svg>Menu </button> </div> <div class="menu-primary-container"><ul id="top-menu" class="menu"><li id="menu-item-1189" class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-1189"><a href="https://www.eventbrite.com/engineering/" aria-current="page">Home</a></li> <li id="menu-item-1133" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-1133"><a href="https://www.eventbrite.com/engineering/category/front-end/">Front-End</a></li> <li id="menu-item-1137" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-1137"><a href="https://www.eventbrite.com/engineering/category/performance/">Performance</a></li> <li id="menu-item-1129" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-1129"><a href="https://www.eventbrite.com/engineering/category/mobile/">Mobile</a></li> <li id="menu-item-1141" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-1141"><a href="https://www.eventbrite.com/engineering/category/conferences/">Conferences</a></li> <li id="menu-item-1135" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-1135"><a href="https://www.eventbrite.com/jobs">Jobs</a></li> </ul></div> </nav><!-- #site-navigation --> </div><!-- .wrap --> </div><!-- .navigation-top --> </div><!-- .site-branding-text --> </div><!-- .wrap --> </div><!-- .site-branding --> </div><!-- .custom-header --> </header><!-- #masthead --> <div class="site-content-contain"> <div id="content" class="site-content"> <div class="wrap"> <header class="page-header"> <h2 class="page-title">Posts</h2> </header> <div id="primary" class="content-area"> <main id="main" class="site-main" role="main"> <article id="post-9848" class="post-9848 post type-post status-publish format-image hentry category-uncategorized post_format-post-format-image"> <header class="entry-header"> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/mvi-model-view-intent-architecture-in-android/" rel="bookmark"><time class="entry-date published" datetime="2024-05-22T07:37:19-07:00">May 22, 2024</time><time class="updated" datetime="2024-05-22T07:37:21-07:00">May 22, 2024</time></a></div><!-- .entry-meta --><h3 class="entry-title"><a href="https://www.eventbrite.com/engineering/mvi-model-view-intent-architecture-in-android/" rel="bookmark">MVI [Model View Intent] architecture in Android</a></h3> </header><!-- .entry-header --> <div class="entry-content"> <p><strong>Author: </strong>Karishma Agrawal, Senior Android Engineer, Eventbrite India</p> <p>At Eventbrite, our Android apps (<a href="https://play.google.com/store/apps/details?id=com.eventbrite.organizer&amp;pcampaignid=web_share">Organizer App</a> and <a href="https://play.google.com/store/apps/details?id=com.eventbrite.attendee&amp;pcampaignid=web_share">Attendee App</a>) are based on MVI [Model View Intent] architecture pattern that is often attributed to Cycle.js, a JavaScript framework developed by <a href="https://twitter.com/andrestaltz">André Staltz</a>. MVI has been adopted and adapted by various developers and communities across different programming languages and platforms. This article will throw light on MVI architecture, its benefits and how different it is from MVVM.</p> <p><strong>Model: </strong>The Model represents the data and business logic of the application. In MVI, the Model is immutable and represents the current state of the application.</p> <p><strong>View:</strong> The View is responsible for rendering the UI and reacting to user input. However, unlike MVVM and MVC, the View in MVI is a passive component. It does not directly interact with the Model or make decisions based on the data. Instead, it receives state updates and user intents from the ViewModel.</p> <p><strong>Intent:</strong> The Intent represents user actions or events that occur in the UI, such as button clicks or text input. In MVI, these intents are captured by the View and sent to the ViewModel for processing.</p> <p><strong>ViewModel: </strong>The ViewModel in MVI is responsible for managing the application state and business logic. It receives user intents from the View, processes them, and updates the Model accordingly. The ViewModel then emits a new state, which the View observes and renders.</p> <p>Let’s understand MVI with a flow from the Eventbrite app. Let’s apply the concept of Model — View — Intent.</p> <figure class="wp-block-image"><img decoding="async" src="https://lh7-us.googleusercontent.com/ClrOctCplJ71Yv-mR4WdDBOxLSSYvawtqfzIGj9uDfTCCn5yUMReFEa8rd-ga5kJ1CjED6_BNM8zLjdWdV7GzJPeQiH0Ulxw1lw2hJuCL_A4zkSxl7htgkdkzK_DT8ioVJ6CPhEXS5dtB1rP-smi8p8" alt=""/></figure> <p>This is an Event Detail page in the Attendee app. Users can access this page from typically two places, one Event list and another from search.</p> <p>This page shows details about an event like Name, Date , time , place, hosted by, summary of event. And there are some clicks: Like, Unlike, share, Follow Creator, Get Tickets, etc.</p> <p>Let’s understand its implementation step by step using MVI.</p> <p># Model</p> <p>ViewState</p> <p>Advantage Over MVVM<br>State management: MVI provides a clear and centralized approach to managing the application state. By representing the state as an immutable Model and handling state updates in the ViewModel, MVI reduces the complexity of managing state changes, compared to MVVM where state management can become fragmented across multiple ViewModels.</p> <p>For our Event Detail page we can have following states:</p> <ul class="wp-block-list"> <li>Loading</li> <li>Content</li> <li>Error</li> </ul> <p>These are 3 basic states for each screen.</p> <pre class="wp-block-code"><code>internal sealed class ViewState { @Immutable class Loading(val onBackPressed: () -> Unit = {}) : ViewState() @Immutable class Content(val event: UiModel) : ViewState() @Immutable class Error(val error: ErrorUiModel): ViewState() }</code></pre> <p>The initial State for Screen is Loading. We will show a progress bar until we are not finished fetching event details from the server.</p> <figure class="wp-block-image"><img decoding="async" src="https://lh7-us.googleusercontent.com/HV6JacbtnSTgijt6wxJrNwZxTveaFha_x3g2UXyV5buLncbylN7DstCjTGH6ueoFHwVQP51vlhQZx_klPCgjivbVcvEAstKmGyB114ySwZ0_bO_ZlJA0TMCeNO-M7xV5WwEgpbv25AMh4wZfHofw1R0" alt=""/></figure> <p>In Compose we will check the state and Load View accordingly</p> <pre class="wp-block-code"><code>@Composable internal fun Screen( state: State, ) { when (state) {     is State.Loading -> Loading()     is State.Error -> Error(state.error)     is State.Content -> Content(state.event) } }</code></pre> <p>So now whenever you want to change UI, you don&#8217;t change it directly but communicate to the state and UI will Observe the state to make changes.</p> <p># Intent</p> <p>Events</p> <p><strong>Advantage Over MVVM</strong><strong><br></strong>Data flow: In MVI, the unidirectional data flow from View to ViewModel to Model simplifies the flow of data and events in the application. This ensures a predictable and consistent behavior, making it easier to reason about the application’s behavior compared to the bidirectional data binding in MVVM.</p> <figure class="wp-block-image"><img decoding="async" src="https://lh7-us.googleusercontent.com/w24WE11qaLzP3m6RPvUtpkSiphOVgnBkDc05bXSt7Wlod1JQNcJd0CzOxjRX0xTogk-xL1hU81Y_CZsv_s2RNHAUwayurLrWvJCw7Viw1QKHJ6XMFo-YbQrf3ckD-1jotPXDm1On6_tgkmbfzlEgewQ" alt=""/></figure> <p>An event is a sealed class that defines the action.</p> <pre class="wp-block-code"><code>sealed class Event { data object Load : Event() class FetchEventError(val error: NetworkFailure) : Event() class FetchEventSuccess(val event: ListingEvent) : Event() class Liked(val event: LikeableEvent) : Event() class Disliked(val event: LikeableEvent) : Event() class FollowPressed(val user: FollowableOrganizer) : Event() }</code></pre> <figure class="wp-block-image"><img decoding="async" src="https://lh7-us.googleusercontent.com/Va97I6WwrpbjQT1znkqtAQaomjjxzUjjW_ztxd1YMAiTZ6ICTLWegIMdUinWyHyTZ4ulM7a4lOqWCdvdghGyiEdMR9EOKmJEPeHEM2nJD1pwSMPuV_E8UM_NRvOittLyRQh3lHwif0hgtHFXl5t_jKU" alt=""/></figure> <p>Let’s understand each event one by one.</p> <p>Load Event:</p> <figure class="wp-block-image"><img decoding="async" src="https://lh7-us.googleusercontent.com/hj-nw1EeEzobjrn3RGxZdCG0U11uGfFWD0GRw9coV46ttgPOQIJQu6ehRDbpLvyr0bjyPEH8p1Zyd8qs5nHhCvWe8TPRkUwoj1CDQ6YhlXwIaxx6EYPyQd4u9prGFipN2VW0b9EqN1rjdlj5iY8U4M4" alt="Load event"/></figure> <p>Load is an initial event, which gets triggered from Fragment. In OnCreate, we are setting our events. And initial event is Load, which is handled by ViewModel.</p> <pre class="wp-block-code"><code>override suspend fun handleEvent(event: Event) {     when (event) {         is Event.Load -> load()     } }</code></pre> <p>In the Load function then we are fetching event details from the server. On Success or error of this API we change Ui State, which gets observed by UI and UI gets updated Accordingly.</p> <pre class="wp-block-code"><code>&nbsp;getEventDetail.fetch(eventId)<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .fold({ error -&gt;<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state {<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ViewState.Error(<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; error = error.toUiModel(events)<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }) { response -&gt;<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state { ViewState.Content(event.toUiModel(events, effect)) }<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }</code></pre> <p>Receive changes in View</p> <pre class="wp-block-code"><code>internal fun EventDetailScreen( state: ViewState ) { when (state) {     is ViewState.Loading -> Loading()     is ViewState.Error -> Error(state.error)     is ViewState.Content -> Content(state.event) } }</code></pre> <p># Reducer</p> <p>State Reducer is a concept from functional programming that takes the previous state as input and computes a new state from the previous state</p> <p>Let’s understand this with a feature where Attendee follows a creator, what happens when the user clicks on follow.</p> <figure class="wp-block-image"><img decoding="async" src="https://lh7-us.googleusercontent.com/ul2hQqulB3iBUpo_0XFMpCI76132FuBEn8SzoviubtjBpBAoz7iBjUyNcSBlrJu9GMk1-Y3UyG6RVI0vKCWeP0IVfBcUj8E9POzwKiB-BlsErPVP_oGE6J7nUMsKZl52bg4dorABPVhJyGnJ9YrQljQ" alt=""/></figure> <p>FIrst we have an UiModel which contains content state, and using this object we show data on the UI.</p> <pre class="wp-block-code"><code>internal data class UiModel( val eventTitle: String, val date: String, val location: String, val summary: String, val organizerInfo: OrganizerState, val onShareClick: () -> Unit, val onFollowClick: () -> Unit )</code></pre> <p>Now let’s understand this step by step:</p> <figure class="wp-block-image"><img decoding="async" src="https://lh7-us.googleusercontent.com/E-etVnPMoxyCESqWIgNmKFAuACgErFMRpQD01e9EFQfwzoqm_jX23QWTyuui-X13rEdTF8hBJ3V1_OEoQLwfQArZ9yCEP7ERbm0iN_HIKetysUTF7qRDrARynGq6iHxBaEqEvZTKQAoTRzTsymTG-Jk" alt="Follow org flow"/></figure> <p>Action 1: Implement User Click listener and trigger event</p> <pre class="wp-block-code"><code>onClick { events(EventDetailEvents.FollowPressed(followableOrganizer)) }</code></pre> <p>Action 2: Handle Event In ViewModel</p> <p>If the Organizer is Already followed then unFollow them Otherwise Follow them.</p> <pre class="wp-block-code"><code>if (followableOrganizer.isFollowed) { state { onUnfollow(::event, ::effect) } } else { state { onFollow(::event, ::effect) } }</code></pre> <p>Action 3: Reducer</p> <p>onUnFOllow and onFollow is handled by reducer, where it is getting the prev state and modifying it and then sending back to view.</p> <pre class="wp-block-code"><code>private fun getFollowContent(<br>&nbsp; &nbsp; event: UiModel,<br>&nbsp; &nbsp; newState: Boolean,//Shows Following or UnFOllowing<br>&nbsp; &nbsp; events: (Event) -&gt; Unit<br>) = ViewState.Content(<br>&nbsp; &nbsp; event.copy(<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; organizerState = with((event.organizerState as OrganizerState)) {<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; val hasChanged = newState != isFollowing<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; OrganizerState.Content(copy(<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; isFollowing = newState,<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; listeners = OrganizerListeners(<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; onFollowUnfollow = {<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; val followableUser = event.toFollowableModel(newState, it.toBookmarkCategory())<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; events(Event.FollowPressed(followableUser))<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }<br>&nbsp; &nbsp; )<br>)</code></pre> <p>getFollowContent is returning View state</p> <p>Action 4: Return View state from view Model</p> <pre class="wp-block-code"><code>state { onUnfollow(::event, ::effect) }</code></pre> <p>Action 5: Observe this change in View and modify the UI</p> <p><strong>Conclusion</strong></p> <p>In conclusion, adopting the Model-View-Intent (MVI) architecture at Eventbrite has not only enhanced our Android app but also simplified the development process. By embracing MVI, we’ve streamlined state management, improved data flow, and ensured a more predictable and consistent behavior within our applications.</p> <p>The key advantages of MVI over traditional architectures like MVVM are evident. With MVI, we benefit from a clear and centralized approach to state management, where the Model represents the immutable state of the application, the View renders the UI passively based on state updates, and the Intent captures user actions seamlessly. This unidirectional data flow simplifies the flow of data and events, making it easier to reason about our app’s behavior and reducing the complexity often associated with managing state changes in MVVM.</p> <p>Moreover, the implementation of MVI within our Eventbrite app, as demonstrated through the Event Detail page example, showcases its practicality and effectiveness. By defining clear states, handling events, and employing reducers to compute new states, we’ve achieved a more efficient and maintainable codebase.</p> <p>In summary, the adoption of MVI architecture has not only empowered us to build robust and scalable Android apps at Eventbrite but also sets a precedent for simplifying development processes across the board. It&#8217;s clear separation of concerns, predictable data flow, and centralized state management make it a valuable paradigm that every developer should consider incorporating into their projects. With MVI, the path to creating exceptional user experiences through intuitive and well-structured applications becomes clearer and more attainable.</p> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-9844" class="post-9844 post type-post status-publish format-image hentry category-security tag-csrf-protection tag-cybersecurity-best-practices-2 tag-eventbrite-security tag-owasp-guidelines tag-web-security post_format-post-format-image"> <header class="entry-header"> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/combatting-csrf-at-eventbrite/" rel="bookmark"><time class="entry-date published" datetime="2024-04-23T15:44:57-07:00">April 23, 2024</time><time class="updated" datetime="2024-04-23T15:45:00-07:00">April 23, 2024</time></a></div><!-- .entry-meta --><h3 class="entry-title"><a href="https://www.eventbrite.com/engineering/combatting-csrf-at-eventbrite/" rel="bookmark">Combatting CSRF at Eventbrite: Safeguarding Strategy</a></h3> </header><!-- .entry-header --> <div class="entry-content"> <h2 class="wp-block-heading"><strong>Background</strong></h2> <p>Cross-Site Request Forgery (CSRF), also known as “Sea Surf,” Session Riding, Hostile Linking, or one-click attacks, is a prevalent web security vulnerability that exploits users&#8217; trust in websites to execute unauthorized actions. In a CSRF attack, an attacker tricks a victim into unwittingly performing actions on a trusted website. This is typically achieved by enticing the victim to click on a URL or image embedded in a message on social platforms or other online channels.&nbsp;</p> <p>Once the victim interacts with the malicious link or image, the attacker can execute various actions on the victim&#8217;s behalf without their knowledge. This poses a significant threat as CSRF attacks can manipulate the victim&#8217;s session on the targeted website, allowing the attacker to perform actions as if they were the victim.</p> <p>CSRF attacks predominantly exploit websites lacking mechanisms to distinguish between legitimate user requests and unauthorized ones generated without user consent. These attacks typically aim to execute state-changing actions, such as modifying email addresses, altering passwords, or initiating unauthorized fund transfers.</p> <h2 class="wp-block-heading">Real-World CSRF examples</h2> <p>Several noteworthy instances of CSRF vulnerabilities have been identified in the past, prompting remedial actions from affected organizations. For instance, in 2008, ING Direct, a Dutch-based multinational banking group, faced a CSRF vulnerability within its banking website, enabling attackers to execute fund transfers from users&#8217; accounts without their consent. Similarly, in the same year, the uTorrent website experienced a CSRF attack, resulting in the widespread distribution of malware through downloads.</p> <p>In 2014, McAfee&#8217;s website was found to be susceptible to CSRF attacks<sup>[1]</sup>, which allowed malicious users to tamper with other users&#8217; accounts. Furthermore, in 2020, TikTok encountered a CSRF vulnerability<sup>[3]</sup>, enabling attackers to distribute malware-laden messages to unsuspecting users. Subsequently, once the malware was deployed, malicious actors could initiate requests from the compromised accounts on behalf of the users. These incidents underscore the importance of promptly addressing and rectifying CSRF vulnerabilities to safeguard user data and mitigate potential risks.</p> <h2 class="wp-block-heading"><strong>Mitigation Mechanism</strong></h2> <p>As per the OWASP community guidelines on &#8216;Cross-Site Request Forgery Prevention,&#8217; the foremost and widely endorsed approach to mitigate CSRF attacks involves the utilization of CSRF tokens, alternatively termed as &#8216;synchronizer tokens&#8217; which are generated server-side. These tokens are securely generated secrets, characterized by high unpredictability and uniqueness per request for each user. Upon each request, the validity of these tokens is verified, and their values are cross-checked to ensure authenticity. If an expired token or a mismatch in token value is detected, the request is promptly aborted, thereby preventing the execution of unauthorized actions.</p> <h2 class="wp-block-heading"><strong>Security Protocols at Eventbrite</strong></h2> <p>Security team at Eventbrite is responsible for all aspects of information security across the organization. Comprising seasoned security professionals, this team is committed to conducting thorough security assessments and setting stringent security protocols.&nbsp;</p> <h4 class="wp-block-heading">Shielding against CSRF</h4> <p>One such measure includes the implementation of a robust CSRF token mitigation system.&nbsp; In 2016, server-side CSRF protection was enforced to all HTTP requests across the website at Eventbrite. A unique, unpredictable token is generated along with each action which effectively safeguards our website against CSRF attacks.&nbsp;</p> <h4 class="wp-block-heading">Additional line of defense &#8211; Admin Portal</h4> <p>To enhance our website&#8217;s defense against CSRF attacks, as a part of their routine, thorough checks, our security team detected a potential vulnerability&nbsp; in the present CSRF protection. This came to light when we encountered an attack that exploited the re-use of a CSRF token to execute a harmful action, changing an admin user&#8217;s password.&nbsp;</p> <p>To address this priority concern, we implemented an additional layer of security exclusive for our internal admin portal. The initial step involved restricting HTTP requests solely to the admin portal subdomain, ensuring they originate from the same source. This effectively limits cross-origin access to the admin subdomain. Furthermore, we introduced an enhanced approach to CSRF token generation exclusively for admin portals, ensuring tokens are more unique and secure. These tokens are now form-specific and generated for every submit action within the admin portal.</p> <p>In this comprehensive approach, we&#8217;ve strictly prohibited calls across domains that attempt to utilize CSRF tokens originally generated from the customer-facing domain to access the admin subdomain. This bolsters the efficiency of our security measures further enhancing our defense against potential threats.&nbsp;</p> <h5 class="wp-block-heading"><strong>References</strong>:</h5> <ol class="wp-block-list"> <li>Blatz, Jeremiah. CSRF: Attack and Defense. 2011. 21 February 2013 . </li> <li>OWASP. Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet. 21 October 2012.</li> <li><a href="https://learn.snyk.io/lesson/csrf-attack/">https://learn.snyk.io/lesson/csrf-attack/</a></li> <li>https://research.checkpoint.com/2020/tik-or-tok-is-tiktok-secure-enough/</li> </ol> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-9822" class="post-9822 post type-post status-publish format-image hentry category-architecture category-back-end category-search-recommendations tag-event-search-optimization tag-machine-learning-for-search tag-opensearch-integration tag-relevance-tuning tag-user-engagement-metrics post_format-post-format-image"> <header class="entry-header"> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/automating-relevance-tuning-for-event-search/" rel="bookmark"><time class="entry-date published" datetime="2024-03-12T11:56:09-07:00">March 12, 2024</time><time class="updated" datetime="2024-03-12T11:58:47-07:00">March 12, 2024</time></a></div><!-- .entry-meta --><h3 class="entry-title"><a href="https://www.eventbrite.com/engineering/automating-relevance-tuning-for-event-search/" rel="bookmark">Automating Relevance Tuning for Event Search</a></h3> </header><!-- .entry-header --> <div class="entry-content"> <p class="has-small-font-size">Authored by: <a href="/engineering/author/zelal/">Zelal Gungordu</a> and <a href="/engineering/author/delaine/">Delaine Wendling</a>. </p> <h2 class="wp-block-heading">1 Background</h2> <p>Relevance tuning is the process of making incremental changes to a search algorithm to improve the ranking of search results to better meet the information needs and preferences of users. Ideally, each such change would improve the quality of results, which can be measured using engagement metrics by running online experiments.</p> <p>Until recently, relevance tuning for event search at Eventbrite was entirely manual. We manually tuned the weights/boosts for different relevance signals used in our search algorithm<sup>1</sup>. We used offline search evaluation to measure the impact of changes against a test set using well-known search metrics such as <em>precision@k</em>, <em>recall@k</em>, <em>mean reciprocal rank</em>, <em>expected reciprocal rank</em>, and <em>normalized discounted cumulative gain</em><sup>2</sup>. Finally, we ran online experiments to measure the impact of changes on engagement metrics such as <em>click-through rate</em> (CTR)<sup>3</sup> and <em>order conversion rate</em> (CVR)<sup>4</sup>. </p> <p>There are a number of drawbacks to manual relevance tuning as summarized below:</p> <ol class="wp-block-list"> <li>When we pick the different values to try out for a given weight/boost in an experiment, there’s no mechanism for us to verify that those are the best values to try. Consequently, when an experiment variant <em>wins</em>, there’s no way for us to verify that the value we chose for that variant is in fact the optimal value for that weight/boost. All we know is that the <em>winning</em> value did better than the other one(s) we tried.</li> <li>Once we pick a value for a given weight/boost through experimentation, we generally do not go back and try to fine-tune the same weight/boost again at a later time. Consequently, manual tuning doesn’t offer us a mechanism to capture any impact of changes in our index contents and/or user behavior, due to seasonality and/or trends, on the relative importance of relevance signals.</li> <li>Our search index includes events from over a dozen different countries. However, a manual tuning-based approach to search relevance is very much one-size-fits-all. For in-person events, we apply a geo-filter to make sure we only return events for the given user&#8217;s location. However, when it comes to ranking events, we use the exact same relevance signals with the exact same weight/boost values for any location. Consequently, there is no mechanism to capture the differences in content and/or user behavior between different countries or regions that may have an impact on the relative importance of relevance signals.</li> </ol> <p>Now that we have summarized the manual approach to relevance tuning and its drawbacks, let’s discuss how automating relevance tuning can address these concerns.</p> <h3 class="wp-block-heading">1.1 Automating Relevance Tuning</h3> <p>Automating relevance tuning involves applying machine learning techniques to tune the weight/boost values of relevance signals through a technique called Learning to Rank. Learning to Rank (LTR), also known as Machine-Learned Ranking (MLR), is the application of machine learning (ML) in training and using models for ranking optimization in search systems<sup>5</sup>. Instead of manually tuning weight/boost values for relevance signals, the LTR process trains an ML model that can learn the relative importance of signals from the index contents and training data, which consists of relevance judgments (i.e., search queries mapped to correctly-ranked lists of documents). The resulting model is then used at query time to score search results.</p> <p>LTR can leverage either explicit relevance judgments (using manually labeled training data) or implicit judgments (derived from user signals such as clicks and orders). Using implicit judgments is a better fit for us for the following reasons:</p> <ol class="wp-block-list"> <li>Curating explicit judgments at a scale to support effective LTR can be very time-consuming and expensive.</li> <li>Our search index is fairly dynamic in that we have new events created on a regular basis while old events get purged as they become obsolete. Typically, the index contents change completely every six months. If we were to use explicit judgments, we would need to gather them on a fairly regular basis to make sure they don’t go stale as the index contents change.</li> <li>Using implicit judgments based on user signals allows us to train a model that optimizes relevance to improve user experience rather than someone’s perception of relevance expressed through explicit judgments. Note that user signals may give us clues beyond whether an event is relevant to a search query based on its title and description. We may be able to determine how much other factors like event quality, date, and location weigh in on users’ decisions to engage with an event.</li> </ol> <p>By using implicit judgments based on user signals, we essentially introduce a self-learning feedback loop in our search system where our users’ interactions with search results help the system self-tune its results to improve future user experience.</p> <p>Now that we have defined what we mean by automating relevance tuning, let’s go back to the drawbacks we listed in the previous section for manual relevance tuning and explain how LTR using implicit judgments help us address those concerns.</p> <ol class="wp-block-list"> <li>LTR frames relevance tuning as an optimization problem. We essentially optimize relevance against an objective function<sup>6</sup> while trying to maximize the value of our chosen relevance metric<sup>7</sup>. Consequently, the model learns through the training process the optimal weight/boost values for the relevance signals within the problem space we define during training, which consists of the training data derived from implicit judgments, the index contents and the list of features we choose for our model (i.e., relevance signals).</li> <li>Using implicit judgments based on user signals help us capture the latest trends in user behavior. We have the ability to automate training new models on a regular basis – on a cadence determined by&nbsp; business needs – to capture the latest trends in user behavior as well as our index contents.</li> <li>Instead of a one-size-fits-all approach to relevance tuning, we can train different models for different locations (countries or regions) as long as there’s sufficient user traffic to help derive reliable relevance judgments for those locations. This is possible because LTR using implicit judgments is completely language-agnostic, since the training data is derived from user signals.</li> </ol> <h3 class="wp-block-heading">1.2 Deriving Relevance Judgments from Implicit Feedback</h3> <p>We have talked about using user signals to derive implicit relevance judgments. Note that using raw user signals to derive relevance judgments is prone to a number of problems.</p> <ol class="wp-block-list"> <li>Users are more likely to click on higher ranked search results than lower ranked ones because they intuitively trust the search system’s judgment on relevance. This is known as <em>position bias</em>.</li> <li>Using a simple metric like CTR results in search results with fewer user interactions leading to less reliable judgment outcomes than those with more interactions.</li> <li>If search never surfaces certain events in the first place, users will not get a chance to interact with them. So, those events will never get a chance to accrue user signal data.</li> </ol> <p>For these reasons, rather than using raw signals, it is common to use <em>click models</em> to derive relevance judgments from user signals. Click models are probabilistic models of user behavior that aim to predict future user behavioral patterns by analyzing historical signal data<sup>8</sup>. They provide a reliable way to translate implicit signals into unbiased relevance judgment labels.</p> <h2 class="wp-block-heading">2 Our Approach to Automating Relevance Tuning</h2> <p>In this section, we summarize our approach to automating relevance tuning for event search.</p> <p>We use the <a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/learning-to-rank.html">OpenSearch LTR plugin</a> for automating relevance tuning. This is an open-source plugin available for all OpenSearch distributions. It’s also one of the plugins supported by Amazon OpenSearch Service. With this plugin, judgment list generation, training data preparation, and model training take place outside of Amazon OpenSearch Service. Once a model is trained, it is deployed to OpenSearch and stored in the same OpenSearch cluster as the search index. Using the model at query time involves performing a request with a special query type supported by the LTR plugin called <em>sltr</em> query – which takes the query keywords and the name of the model to use to score results. This is done within the context of an OpenSearch <em>rescore</em> query. In this case, the <em>query</em> part of the request is used to retrieve and rank results using the BM25 scoring function in the usual way<sup>9</sup>. Then, the top <em>k</em> results following BM25 ranking are rescored/reranked using the LTR model specified in the <em>sltr</em> query<em>.</em> The LTR model is used only in the rescoring phase because it’s a more expensive scoring technique than BM25 scoring.</p> <h3 class="wp-block-heading">2.1 Using the OpenSearch LTR Plugin</h3> <p>As mentioned earlier in this section, with the LTR plugin, judgment list generation, training data preparation, and model training takes place outside of Amazon OpenSearch Service. This section explains training data preparation and model training. The best resource to understand how to work with the LTR plugin is the <a href="https://elasticsearch-learning-to-rank.readthedocs.io/en/latest/">documentation</a> for the Elasticsearch LTR plugin. The OpenSearch LTR plugin is just a fork of the Elasticsearch plugin. In this section, we will summarize some of the information from that documentation. The reader is advised to read the documentation itself if interested in finding out more about the plugin. Another good resource for this plugin is the <a href="https://github.com/o19s/hello-ltr">hello-ltr</a> Python repo that illustrates how to use the Elasticsearch LTR plugin.</p> <h4 class="wp-block-heading">Training Data Preparation</h4> <p><a href="https://elasticsearch-learning-to-rank.readthedocs.io/en/latest/core-concepts.html">This section</a> of the plugin documentation explains the key concepts used by the LTR plugin. There are two types of input that are required for LTR training data preparation:</p> <ul class="wp-block-list"> <li><strong>Judgment lists:</strong> <a href="https://elasticsearch-learning-to-rank.readthedocs.io/en/latest/core-concepts.html#judgments-expression-of-the-ideal-ordering">Judgment lists</a> are collections of grades for individual search results for a given set of search queries. The following is a sample pseudo-judgments list for event search:</li> </ul> <pre class="wp-block-code has-black-color has-cyan-bluish-gray-background-color has-text-color has-background has-link-color wp-elements-60f749d52976e1bcf39943451888a111"><code># qid:1: jazz # qid:2: valentines # # grade (0-4) queryId # docId title 4 qid:1 # 7555 Jazz Concert 2 qid:1 # 6238 Hip Hop Concert 4 qid:2 # 8125 Valentine's Day</code></pre> <p>This sample file follows the <em>LibSVM</em> format commonly used by LTR systems. Note that the exact format we use is flexible (as long as the code we use to parse the judgments list file is in line with the format requirements). The information required for each judgment tuple consists of the search query in question, the document ID in question and how relevant that document is to that query, i.e., grade. In this sample, we are told that the event with the ID <em>7555</em> is very relevant to the search query <em>jazz</em>. The event title is included in the judgment only for human-readability purposes.</p> <ul class="wp-block-list"> <li><strong>Features</strong>: <a href="https://elasticsearch-learning-to-rank.readthedocs.io/en/latest/core-concepts.html#features-the-raw-material-of-relevance">Features</a> are essentially the relevance signals we use in our search algorithm; e.g., title match, description match, event quality boost, etc. The LTR plugin expects features to be expressed as OpenSearch queries. For example, to use title match as a relevance signal we include a feature like the following:</li> </ul> <pre class="wp-block-code has-black-color has-cyan-bluish-gray-background-color has-text-color has-background has-link-color wp-elements-b9f34c3a210a8fdfa3514552f0e02ba8"><code>{ "query": { "match": { "title": "{{keywords}}" } } }</code></pre> <p>Once we have defined our judgments list and features, the next step is <a href="https://elasticsearch-learning-to-rank.readthedocs.io/en/latest/core-concepts.html#logging-features-completing-the-training-set">logging features</a>, which essentially means computing scores for each relevance feature based on the index contents. The resulting scores are used to annotate the judgments list to indicate the score for each feature in each judgment tuple. The resulting file would look something like the following snippet, assuming we have two relevance features: <em>title_match</em> and <em>description_match</em>.</p> <pre class="wp-block-code has-black-color has-cyan-bluish-gray-background-color has-text-color has-background has-link-color wp-elements-a8c95a7025c930fe5f226ec82c1d6c13"><code>4 qid:1 title_match:12.3184 description_match:9.8376 # 7555 jazz 2 qid:1 title_match:0 description_match:2.3624 # 6238 jazz 4 qid:2 title_match:9.6778 description_match:5.7859 # 8125 valentines</code></pre> <p>This is the training data that is used to train an LTR model.</p> <h4 class="wp-block-heading">Training an LTR Model</h4> <p>The OpenSearch LTR plugin supports two libraries for training LTR models:</p> <ol class="wp-block-list"> <li><a href="https://sourceforge.net/p/lemur/wiki/RankLib/">RankLib</a>: RankLib is a Java library that includes implementations of eight different LTR algorithms. It is relatively old and does not enjoy the same widespread use as the second option.</li> <li><a href="https://xgboost.readthedocs.io/en/stable/">XGBoost</a>: XGBoost is an optimized distributed gradient boosting library that is very popular. It’s designed to be highly efficient, flexible and portable. It supports multiple languages, including a <a href="https://xgboost.readthedocs.io/en/stable/python/index.html">Python package</a>.</li> </ol> <p>We use XGBoost for LTR training. Note that XGBoost offers a variety of <a href="https://xgboost.readthedocs.io/en/stable/parameter.html#parameters-for-tree-booster">hyperparameters</a> that can be tuned to improve model performance in a given problem space. A good rule of thumb is to start with the default values and apply some mechanism like grid search combined with cross validation to tune a subset of hyperparameters. Here’s a sample <a href="https://towardsdatascience.com/pair-wise-hyperparameter-tuning-with-the-native-xgboost-api-2f40a2e382fa">blog post</a> that illustrates the process of hyperparameter tuning with XGBoost. The XGBoost hyperparameters for LTR are documented <a href="https://xgboost.readthedocs.io/en/stable/parameter.html#parameters-for-learning-to-rank-rank-ndcg-rank-map-rank-pairwise">here</a>.</p> <h4 class="wp-block-heading">Uploading an LTR Model to OpenSearch and Using it at Search Time</h4> <p>Once we train an LTR model, we <a href="https://elasticsearch-learning-to-rank.readthedocs.io/en/latest/training-models.html#uploading-a-model">upload</a> it to our OpenSearch cluster. Note that one can upload and store multiple models on the cluster. The LTR plugin’s <em>sltr</em> query that’s used at <a href="https://elasticsearch-learning-to-rank.readthedocs.io/en/latest/searching-with-your-model.html">search time</a> takes a particular model as an argument. Hence, it’s possible to create and store different models for, say, different geographical regions, and then use the model corresponding to the given user’s location at search time.</p> <h3 class="wp-block-heading">2.2 LTR Architecture</h3> <figure class="wp-block-image"><img decoding="async" src="https://lh7-us.googleusercontent.com/PWIqZdjEGvTo4EeCmvq0iC6tM7lPsUS4rbBiBdefkmnSBCuneaAS55mjdmH4cRRVmStLKLVF4a1iex6Ggfe21PQaAIOYsJa6YnTOAhMD2Ch7fvXr1Xye_6WsfvgK2KVSEH5VYhAML2PXzRdx_I_fEgk" alt="A conceptual illustration depicting the process of automating relevance tuning for event search, showcasing interconnected nodes representing machine learning techniques, user signals, and search algorithms, symbolizing the optimization of search results for improved user experience."/></figure> <p>We use an Airflow Directed Acyclic Graph (DAG) to pull user signal data from a Snowflake table and transform it into a format from which we can generate implicit judgments. The DAG runs daily and invokes a Lambda function that triggers a Step Function. This Step Function orchestrates the invocation of three Lambdas that clean and reformat the user signal data, generate implicit judgments and call OpenSearch to compute the feature scores for each event mentioned in the implicit judgements. This process runs automatically every day.</p> <p>We have a separate Lambda function that trains the LTR model and stores it in OpenSearch, as well as S3 for backup purposes. This Lambda function utilizes training data (implicit judgments with features) from a range of dates determined by the parameters passed to the Lambda to train the model. Once the model is uploaded to OpenSearch, it can be used at search time. Note that prior to rolling out a new model on our production systems, we perform offline evaluation against a test set using the well-known search metrics mentioned in Section 1. This helps us tune the rescore query window size used to decide how many of the top <em>k</em> search results we should rescore using the LTR model. It also helps us tune the rescore query weight used to weigh the relevance scores returned by the model while computing the overall relevance scores for search results. Finally, we run an online experiment where we start using the new model as part of search in one of the experiment variants and track our usual engagement metrics to measure the impact on user experience.</p> <h2 class="wp-block-heading">3 Future Work</h2> <p>We are still at the start of our journey for automating relevance tuning. We have many areas where we plan to iterate to improve our current approach.</p> <p>One of the initial areas where we plan to invest in is improving our feature set. Currently, we use a limited number of features that consist of lexical features. We plan to expand this set to include embedding-based features to capture the semantic understanding of search queries.</p> <p>So far, we have assumed that the LTR model would be used for requests with an actual search query. However, there’s a considerable amount of traffic on our site where the request lacks a search query. We call these requests <em>queryless searches</em>. For example, most of the traffic coming from the home page and other browse surfaces falls in this category. We plan to train separate models for queryless traffic since, without a search query, other relevance signals like date and event quality would have a bigger impact in ranking outcomes.</p> <p>Another area of future work is to figure out when it would make sense to train location-specific models. Would it only make sense to have such models for different countries/regions, or is there enough variation in user behavior and/or index contents across multiple large metropolitan areas even within the same country to warrant them having their own models?</p> <p>Note that so far we have focused on training generalized models of relevance. However, personalizing search results to better meet users’ specific information needs and preferences has proven to improve user engagement and satisfaction. Search personalization is an area we plan to focus on in the near future. Last but not least, traditional LTR approaches like the one we’ve described in this post are supervised ML approaches. They require handcrafted features based on query, document, and how well the two match each other. Over time, heavily engineered features can result in diminishing returns. Another future area of work is to augment the current approach with a neural LTR model. The advantage of neural LTR systems is that, instead of relying on handcrafted features, they allow feature representations to be learned directly from the data. <a href="http://bendersky.github.io/res/TF-Ranking-ICTIR-2019.pdf">This</a> slide deck provides an overview of neural LTR. <a href="https://github.com/tensorflow/ranking/tree/master">Tensorflow Ranking</a> is a library that provides support for neural LTR techniques built on the Tensorflow platform. Generally speaking, neural LTR is recommended to be employed alongside traditional LTR systems in an ensemble setting (as opposed to replacing them) because the strengths of the two approaches are believed to complement each other.</p> <hr class="wp-block-separator has-alpha-channel-opacity"/> <ol class="wp-block-list"> <li>We use around two dozen relevance signals in our search algorithm, ranging from field boosts to ranking functions based on event quality, sales status, location and date.</li> <li><a href="https://amitness.com/2020/08/information-retrieval-evaluation/">This</a> blog post does a very nice job of explaining these metrics with easy-to-follow examples.</li> <li><em>Click-through rate</em> (CTR) is the ratio of the number of clicks to the number of impressions.</li> <li><em>Order conversion rate</em> (CVR) is the ratio of the number of orders to the number of clicks.</li> <li>There is a vast amount of literature on LTR. For a quick overview, see <a href="https://en.wikipedia.org/wiki/Learning_to_rank">this Wikipedia page</a> and <a href="https://towardsdatascience.com/learning-to-rank-a-complete-guide-to-ranking-using-machine-learning-4c9688d370d4">this blog post</a>.</li> <li>For more on objective functions used in LTR, see <a href="https://medium.com/@nikhilbd/pointwise-vs-pairwise-vs-listwise-learning-to-rank-80a8fe8fadfd">this blog</a> post.</li> <li>For more on metrics typically used in LTR, see this Wikipedia <a href="https://en.wikipedia.org/wiki/Learning_to_rank#Evaluation_measures">page</a>.</li> <li>For a thorough overview of click models, see <a href="http://clickmodels.weebly.com/uploads/5/2/2/5/52257029/mc2015-clickmodels.pdf"><em>Click Models for Web Search</em></a> by Chuklin, Markov, and de Rijke.</li> <li><a href="https://opensearch.org/docs/1.1/opensearch/rest-api/explain/">BM25</a> is OpenSearch’s default similarity function used to calculate relevance scores.</li> </ol> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-9782" class="post-9782 post type-post status-publish format-standard hentry category-product"> <header class="entry-header"> <h3 class="entry-title heading-large"><a href="https://www.eventbrite.com/engineering/product-development-process/" rel="bookmark">The Power of Collaboration in Product Development</a></h3> <div class="post-thumbnail"> <a href="https://www.eventbrite.com/engineering/product-development-process/"> </a> </div><!-- .post-thumbnail --> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/product-development-process/" rel="bookmark"><time class="entry-date published" datetime="2023-01-12T17:46:59-08:00">January 12, 2023</time><time class="updated" datetime="2023-06-23T10:11:33-07:00">June 23, 2023</time></a></div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p><span style="font-weight: 400;">Product development at Eventbrite is a practice centered around understanding what our customers need, so we can enhance current features or build new products. In order to achieve this, our product team collaborates across multiple disciplines throughout the company to ensure we’re thinking about customer needs from all angles.</span></p> <h3><b>Who is involved in product development?</b></h3> <p><span style="font-weight: 400;">At the heart of product development is collaboration. We unite knowledge and expertise from different teams across the company, including Research, Product Analytics, Product Marketing, Engineering, Design, and Product Management, to be able to make the most-informed decisions about what we can improve on and build for our customers. Our Customer Experience teams also play a critical role in providing important feedback from creators and attendees in the process.</span></p> <h3><b>What is the product development process itself?</b></h3> <p><span style="font-weight: 400;">Every day, our product development team gathers data and information about how our customers use and engage with the Eventbrite platform. We also have a research team that regularly surveys and interviews both creators and attendees to hear directly from them what’s working and what isn’t to uncover opportunities and key problems. </span></p> <p><span style="font-weight: 400;">This includes analysis of purchasing and engagement patterns on our websites and mobile apps; review of direct customer feedback and app reviews; market research through customer interviews or surveys; and we also evaluate results from past initiatives to learn what has resonated with customers. </span></p> <p><span style="font-weight: 400;">Additionally, we conduct competitive research across the industry and of other best-in-class products to ensure we&#8217;re aligned with best practices, as well as identify opportunities to differentiate and innovate on our solutions. All of this evidence is gathered together and synthesized into clear product goals and problem statements to guide our development teams toward solving a common goal.</span></p> <h3><b>Our collaborative process in action</b></h3> <p><span style="font-weight: 400;">We recently launched</span> <span style="font-weight: 400;">changes to our </span><a href="https://www.eventbrite.com/support/articles/en_US/How_To/how-to-collect-information-from-all-event-attendees?lg=en_US" target="_blank" rel="noopener"><span style="font-weight: 400;">checkout order form</span></a><span style="font-weight: 400;"> where creators can collect information from ticket buyers and ask them specific questions that could be useful for demographic or marketing purposes. However, because these questions were asked before a ticket buyer completes payment, this caused some friction for consumers in the checkout process and may have resulted in them not buying a ticket. This was the process for how we solved it:</span></p> <p><span style="font-weight: 400;">Our design team facilitated a workshop where we leveraged “design sprint” techniques to brainstorm, prototype, and validate ideas. The whole cross-functional team came together to sketch ideas, discuss what will have the biggest impact on customers, and then validate them by showing draft ideas to users to get quick feedback. At this stage, our engineering team started to define the technical aspects of our implementation. </span></p> <p><img fetchpriority="high" decoding="async" class="alignnone wp-image-9785 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Sketching-ideas-and-defining-technical-aspects.jpg" alt="" width="512" height="374" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Sketching-ideas-and-defining-technical-aspects.jpg 512w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Sketching-ideas-and-defining-technical-aspects-300x219.jpg 300w" sizes="(max-width: 512px) 100vw, 512px" /><img decoding="async" class="alignnone wp-image-9784 size-full" style="font-size: 1rem;" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Product-Dev-Ideation-Workshop.jpg" alt="" width="512" height="384" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Product-Dev-Ideation-Workshop.jpg 512w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Product-Dev-Ideation-Workshop-300x225.jpg 300w" sizes="(max-width: 512px) 100vw, 512px" /></p> <p><i><span style="font-weight: 400;">Sketching ideas and defining technical aspects</span></i></p> <p><span style="font-weight: 400;">As our development team started to hone in on the solution of moving certain aspects of the checkout order fields from</span><i><span style="font-weight: 400;"> before </span></i><span style="font-weight: 400;">to</span><i><span style="font-weight: 400;"> after</span></i><span style="font-weight: 400;"> a consumer buys a ticket, we got input from various departments, including those in our customer teams, to ensure we’re thinking of different scenarios a user may face. </span></p> <p><span style="font-weight: 400;">After various iterations, we arrived at an agreed-upon set of requirements that we then executed on. We frequently break this down into different phases of release to ensure we can deliver value to customers quickly. </span></p> <p><img decoding="async" class="alignnone wp-image-9787 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Interdisciplinary-Teams-Workshop.jpg" alt="" width="512" height="384" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Interdisciplinary-Teams-Workshop.jpg 512w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Interdisciplinary-Teams-Workshop-300x225.jpg 300w" sizes="(max-width: 512px) 100vw, 512px" /><img loading="lazy" decoding="async" class="alignnone wp-image-9786 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Product-and-Dev-Team.jpg" alt="" width="512" height="384" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Product-and-Dev-Team.jpg 512w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Eventbrite-Product-and-Dev-Team-300x225.jpg 300w" sizes="(max-width: 512px) 100vw, 512px" /></p> <p><i><span style="font-weight: 400;">Eventbrite interdisciplinary teams</span></i></p> <p><span style="font-weight: 400;">In this final phase before release, we tested the new order checkout form and created a rollout plan which introduced the feature smoothly to our platform. This includes preparing our customer teams with information and knowledge about the release so they can assist users with any questions they may have. </span></p> <p><span style="font-weight: 400;">We usually build in a plan that allows us to pause a release and react immediately if any sort of issue gets detected. We also ran an A/B test and randomized the users who got exposure </span><i><span style="font-weight: 400;">before</span></i><span style="font-weight: 400;"> or </span><i><span style="font-weight: 400;">after</span></i><span style="font-weight: 400;"> checkout to measure objectively if it improved their experience, which is a tactic we typically use for any rollout. </span></p> <p><img loading="lazy" decoding="async" class="alignnone wp-image-9788 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Post-checkout-order-form-prototype-tested-with-users.png" alt="" width="512" height="262" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Post-checkout-order-form-prototype-tested-with-users.png 512w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2023/01/Post-checkout-order-form-prototype-tested-with-users-300x154.png 300w" sizes="(max-width: 512px) 100vw, 512px" /></p> <p><i><span style="font-weight: 400;">Post-checkout order form prototype tested with users</span></i></p> <p><span style="font-weight: 400;">The most important aspect of releasing a feature is measuring the outcomes and iterating on it to make sure it is the best experience possible for customers. We may use the data from our A/B test or user feedback after release to develop the next set of enhancements. We always think back to the original customer problem we were trying to solve.</span></p> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-9589" class="post-9589 post type-post status-publish format-standard hentry category-briteling-profiles"> <header class="entry-header"> <h3 class="entry-title heading-large"><a href="https://www.eventbrite.com/engineering/sapna-nair-eventbrite-vp-of-engineering-india/" rel="bookmark">3 Questions With Sapna Nair — Eventbrite’s New VP of Engineering in India</a></h3> <div class="post-thumbnail"> <a href="https://www.eventbrite.com/engineering/sapna-nair-eventbrite-vp-of-engineering-india/"> </a> </div><!-- .post-thumbnail --> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/sapna-nair-eventbrite-vp-of-engineering-india/" rel="bookmark"><time class="entry-date published" datetime="2022-08-02T06:00:22-07:00">August 2, 2022</time><time class="updated" datetime="2022-08-02T06:16:51-07:00">August 2, 2022</time></a></div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>Sapna Nair joins Eventbrite as our new Managing Director and Vice President of Engineering in India. Sapna is a dynamic leader who will lead Eventbrite’s expansion into India and add to our engineering expertise.</p> <p>Her experience building distributed teams will accelerate hiring of top-tier talent in India, helping to deliver on our ambitious technical vision and high-growth business strategy.</p> <p>Learn why Sapna chose Eventbrite and the approach she’s taking to build out her new team with these three questions.</p> <h3><img loading="lazy" decoding="async" class="wp-image-9679 size-large alignnone" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/08/Pic_2-1024x768.jpeg" alt="Sapna Nair" width="525" height="394" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/08/Pic_2-1024x768.jpeg 1024w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/08/Pic_2-300x225.jpeg 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/08/Pic_2-768x576.jpeg 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/08/Pic_2.jpeg 1280w" sizes="(max-width: 525px) 100vw, 525px" /></h3> <h3 class="p-rich_text_section"><b data-stringify-type="bold">Q. What attracted you to Eventbrite?</b></h3> <p>There are three reasons that attracted me to Eventbrite. First, I strongly believe that life is more enjoyable and meaningful when people come together for shared experiences. Eventbrite has built a phenomenal ecosystem that powers event creators all over the world to cultivate connection, build community, and scale their businesses.</p> <p>Second, my discussions with all the leaders of Eventbrite were very candid, inspiring and confident. The leadership had a clear multi-year strategy for the accelerated growth of the company. They have such a strong belief in the mission of the company. The entire experience was so welcoming, indicating the oft-sought after people oriented culture, with a motivating vision.<br /> Third, given the first two reasons, the opportunity to build and grow that same organization ground up in India was a rare chance to leverage my past skills and experience most effectively, and at the same time, I also continue to learn more through this journey.</p> <h3 class="p-rich_text_section"><b data-stringify-type="bold">Q. What excites you most about building and developing engineering teams?</b></h3> <p>I find the opportunity to define the best practices in people, process and technology, on a clean slate — with no bias or baggage — highly challenging and satisfying. Having said that, now contradicting my own earlier statement of having a clean slate, even though there is no bias or baggage for the specific team(s), there always exists a reference with respect to another team in another geography or another company. That makes the entire dynamics very interesting.</p> <p>I love the enormous prospect it offers to coach managers and ICs. Building engineering teams comes with a lot of learning moments. Though I have done it numerous times in the past, every new cycle teaches me something new.</p> <p>There is a very common impression that engineering teams are solely focussed on technology. That is true, but it is also true that engineering teams need to understand the purpose of the use of their technology. That is what triggers their innovation and inspires them to deliver their best. It means engineering teams must remain connected with the geographically distributed business teams and leadership.</p> <p>I am exhilarated when, keeping engineering teams in front and center, I get to bring together all the stakeholders, across different cultures/time zones/accountabilities, with a common purpose of delighting our customers. Ultimately, the pride and satisfaction I see on the faces of our engineers is priceless, when they establish themselves as the CoE, surpassing<br /> all the teething troubles!</p> <h3 class="p-rich_text_section"><b data-stringify-type="bold">Q: How do you prioritize your well-being in a remote-first environment?</b></h3> <div class="p-rich_text_section">Setting clear expectations starting with:</div> <ul class="p-rich_text_list p-rich_text_list__bullet" data-stringify-type="unordered-list" data-indent="0" data-border="0"> <li data-stringify-indent="0" data-stringify-border="0">Remote-first does not equal to 24/7 availability.</li> <li data-stringify-indent="0" data-stringify-border="0">Making my work hours known to all.</li> <li data-stringify-indent="0" data-stringify-border="0">Defining everyone’s accountability</li> <li data-stringify-indent="0" data-stringify-border="0">Defining rules of engagement with all the stakeholders</li> <li data-stringify-indent="0" data-stringify-border="0">Empowering and encouraging others to manage their own flexibility, like declining meetings if it’s not convenient to them.</li> </ul> <p>Advocating use of technology and automation as much as possible (like dashboards, Slack) to reduce online meeting fatigue and avoid information silos. Blocking slots in my calendar for my ‘Me-Time’.</p> <div class="p-rich_text_section"><b data-stringify-type="bold">Looking to join Sapna’s team? She’s hiring! Check out her open roles </b><b data-stringify-type="bold"><a class="c-link" tabindex="-1" href="https://www.eventbritecareers.com/jobs/search?page=1&amp;query=india" target="_blank" rel="noopener noreferrer" data-stringify-link="https://www.eventbritecareers.com/jobs/search?page=1&amp;query=india" data-sk="tooltip_parent" data-remove-tab-index="true">here</a></b><b data-stringify-type="bold">.</b></div> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-9439" class="post-9439 post type-post status-publish format-standard hentry category-architecture category-back-end category-performance"> <header class="entry-header"> <h3 class="entry-title heading-large"><a href="https://www.eventbrite.com/engineering/as-eventbrite-engineering-leans-into-team-owned-infrastructure-or-devops-were-learning-a-lot-of-new-technologies-in-order-to-stand-up-our-infrastructure/" rel="bookmark">Monitoring Your System</a></h3> <div class="post-thumbnail"> <a href="https://www.eventbrite.com/engineering/as-eventbrite-engineering-leans-into-team-owned-infrastructure-or-devops-were-learning-a-lot-of-new-technologies-in-order-to-stand-up-our-infrastructure/"> </a> </div><!-- .post-thumbnail --> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/as-eventbrite-engineering-leans-into-team-owned-infrastructure-or-devops-were-learning-a-lot-of-new-technologies-in-order-to-stand-up-our-infrastructure/" rel="bookmark"><time class="entry-date published updated" datetime="2022-06-13T17:30:56-07:00">June 13, 2022</time></a></div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p><span style="font-weight: 400;">As Eventbrite engineering leans into team-owned infrastructure, or </span><a href="https://about.gitlab.com/topics/devops/"><span style="font-weight: 400;">DevOps</span></a><span style="font-weight: 400;">, we&#8217;re obviously learning a lot of new technologies in order to stand up our infrastructure, but owning the infrastructure also means it&#8217;s up to us to make sure that infrastructure is stable as we continue to release software. Obviously, the answer is that we need to own monitoring our services, but thinking about </span><i><span style="font-weight: 400;">what</span></i><span style="font-weight: 400;"> and </span><i><span style="font-weight: 400;">how</span></i><span style="font-weight: 400;"> to monitor your system requires a different type of mental muscle than day-today software engineering, so I thought it might be helpful to walk through a recent example of how we began monitoring a new service.</span></p> <h2><span style="font-weight: 400;">The Use-Case</span></h2> <p><span style="font-weight: 400;">My team recently launched our first production use-case for our new service hosted within our own infrastructure. This use-case was fairly small but vital, serving data to our permissioning service to help it build its permissions graph in its </span><span style="font-weight: 400;">calculate_permissions</span><span style="font-weight: 400;"> endpoint. Although we were serving data to only a single client, this client is easily the most trafficked service in our portfolio (outside of the monolith we&#8217;re currently decomposing) and </span><span style="font-weight: 400;">calculate_permissions</span><span style="font-weight: 400;"> processes around 2000 requests per second. Additionally, performance is paramount as said endpoint is used by a wide variety of services multiple times within a single user request, so t, so much so that the service has traditionally had direct database access, entirely circumventing the existing service we were re-architecting. </span><b>We needed to ensure that our new service architecture could handle the load and performance demands of a direct database call</b><span style="font-weight: 400;">. If this sounds like a daunting first use-case, you&#8217;re not wrong. We chose this use-case because it would be a great test for the primary advantage of the new service architecture: scalability.</span></p> <h2><span style="font-weight: 400;">The Dashboards</span></h2> <p><span style="font-weight: 400;">For our own service, we created a general dashboard for service-level metrics like latency, error rate and the performance of our infrastructure dependencies like our Application Load Balancer, ECS and DynamoDB. Additionally, we knew that in order for the ramp-up to be successful, we&#8217;d need to closely monitor not only our service&#8217;s performance, but even more importantly, we&#8217;d need to monitor our impact to the permissions service to which we were serving data. For that, we created another dashboard focused on the use case combining metrics from both our service and our client.</span></p> <p><span style="font-weight: 400;">We tracked the performance of the relevant endpoints in both services to ensure we were meeting the target SLOs:<img loading="lazy" decoding="async" class="alignnone size-full wp-image-9449" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/targetSLOs.png" alt="" width="1004" height="378" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/targetSLOs.png 1004w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/targetSLOs-300x113.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/targetSLOs-768x289.png 768w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /></span></p> <p><span style="font-weight: 400;">We added charts for our success metrics, in this case, we wanted to decrease the number of direct-database calls from the permissions service which we watched fall as we ramped up the new service.</span></p> <p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-9459" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/metrics.png" alt="" width="995" height="270" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/metrics.png 995w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/metrics-300x81.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/metrics-768x208.png 768w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /></p> <p><span style="font-weight: 400;">We added metrics inside of our client code to measure how long the permission service was waiting on calls to our service. In the example below, you can see that the client-implementation was causing very erratic latency (which was not visible on the server side). Seeing this discrepancy in performance on the client and server sides, we detected an issue in our client implementation which had a dramatic impact on performance stability. We addressed this volatility by implementing connection pooling in the client. </span></p> <p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-9469" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/changegRPC.png" alt="" width="998" height="430" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/changegRPC.png 998w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/changegRPC-300x129.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/changegRPC-768x331.png 768w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /></p> <p><span style="font-weight: 400;">As the ramp-up progressed, we also added new charts to the dashboard as we tested various theories. For instance, our cache hit rate was underwhelming. We hypothesized that the format of the ramp up (percentage of requests) actually meant that low percentages would artificially lower our cache hit rate so we added this chart to compare the hit rate against similar time periods. It&#8217;s important to keep context in mind; fluctuations may be expected throughout the course of a day or week (I actually disabled the day-over-day comparison below because the previous day was a weekend and traffic was impacted as a result). This new chart made it very easy to confirm our suspicions.</span></p> <p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-9479" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/DAXhitrate.png" alt="" width="998" height="636" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/DAXhitrate.png 998w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/DAXhitrate-300x191.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/DAXhitrate-768x489.png 768w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /></p> <p><span style="font-weight: 400;">This is just a sampling of the data we&#8217;re tracking and the metrics collection we implemented, but the important lesson is that your dashboard is a living project of its own and will evolve as you make new discoveries about </span><i><span style="font-weight: 400;">your specific system</span></i><span style="font-weight: 400;">. Are you processing batch jobs? Add metrics to compare how various batch sizes impact performance and how often you get requests with those batch sizes. Are you rolling out a big feature? Consider metrics that allow you to compare system behavior with and without your feature flag on. Think about what it means for your system or project to be successful and think about what additional metrics will help you quantify that impact.</span></p> <h2><span style="font-weight: 400;">Alerting</span></h2> <p><span style="font-weight: 400;">Monitoring is particularly great when launching a new system or feature and it can be very helpful when debugging problematic behavior, but for day-to-day operations you&#8217;re not likely to pour over your monitors very closely. Instead, it&#8217;s essential to be alerted when certain thresholds are approached or specific events happen. Use what you&#8217;ve learned during monitors to create meaningful alerts (or alarms).</span></p> <p><span style="font-weight: 400;">Let&#8217;s revisit the monitor above that leveled out after a client configuration change.<img loading="lazy" decoding="async" class="alignnone size-full wp-image-9489" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/changegRPC1.png" alt="" width="998" height="430" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/changegRPC1.png 998w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/changegRPC1-300x129.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/06/changegRPC1-768x331.png 768w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /></span></p> <p><span style="font-weight: 400;">I would probably like to know if something else causes the latency to increase that dramatically. We can see that the P95 latency leveled off around 17ms or so. Perhaps I&#8217;d start with an alert triggered when the P95 latency rises above 25ms. Depending on various parameters (time of day, normal usage spikes, etc.), maybe it&#8217;s possible for the P95 to spike that high without the need to sound the alarms, so I&#8217;d set up the alert to only fire when that performance is sustained over a 5 minute period. Maybe I set up that alert, and it goes off 5 times in the first week and we choose not to investigate based on other priorities. In that case, I should consider adapting the alerts (maybe increasing the threshold or span of time) to better align with my team&#8217;s priorities. Alerts should be actionable and the only thing worse than no alert, is an alert that trains your teams to ignore alerts.</span></p> <p><span style="font-weight: 400;">Like with monitoring, there is no cookie-cutter solution for alerting. One team&#8217;s emergency may be business as usual for another team. You should think carefully about what is a meaningful alert to your team based on the robustness of the infrastructure, your real-world usage patterns and any SLA&#8217;s the team is responsible for upholding. Once you&#8217;re comfortable with your alerts, they&#8217;ll make great triggers for your on-call policies. Taking ownership over your own infrastructure takes a lot of work and can feel very daunting at first, but with these tools in place, you&#8217;re much more likely to enjoy the benefits of DevOps (like faster deployments and triage) and spend less time worrying.</span></p> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-9149" class="post-9149 post type-post status-publish format-standard hentry category-back-end category-scalability category-service-oriented-architecture tag-design tag-grpc-service-design tag-infrastructure tag-service-design tag-service-oriented-architecture"> <header class="entry-header"> <h3 class="entry-title heading-large"><a href="https://www.eventbrite.com/engineering/packaging-generated-code-from-protobuf-files-for-grpc-services/" rel="bookmark">Packaging generated code from protobuf files for gRPC Services</a></h3> <div class="post-thumbnail"> <a href="https://www.eventbrite.com/engineering/packaging-generated-code-from-protobuf-files-for-grpc-services/"> </a> </div><!-- .post-thumbnail --> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/packaging-generated-code-from-protobuf-files-for-grpc-services/" rel="bookmark"><time class="entry-date published" datetime="2022-05-02T13:03:16-07:00">May 2, 2022</time><time class="updated" datetime="2022-05-03T06:54:45-07:00">May 3, 2022</time></a></div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <h2><span style="font-weight: 400;">Background</span></h2> <p><span style="font-weight: 400;">At Eventbrite, we identified in our </span><a href="https://www.eventbrite.com/engineering/writing-our-3-year-technical-vision/,"><span style="font-weight: 400;">3-year technical vision</span></a><span style="font-weight: 400;"> that one of our goals is to enable autonomous dev teams to own their code and architecture so as to be able to deliver reliable, high quality and cost effective solutions to our customers. However,  this autonomy does not mean that our team has to work in complete isolation from other teams in order to achieve their goals.</span></p> <p><span style="font-weight: 400;">Over the past year, we have started our transition from our monolithic Django + Python approach to a microservices architecture; we selected </span><a href="https://grpc.io/"><span style="font-weight: 400;">gRPC</span></a><span style="font-weight: 400;"> as our low-latency protocol for inter-microservice communication. One of the main challenges that we face is sharing Protobuf files between teams for generating client libraries. We want it to be as easy as possible by avoiding unnecessary ceremonies and integrating into  team development cycles.</span></p> <h2><span style="font-weight: 400;">Challenges managing Protobuf definitions</span></h2> <p><span style="font-weight: 400;">Since our teams have full autonomy of their code and infrastructure, they will have to share Protobuf files. Multiple sharing  strategies are available, so we identified key questions:</span></p> <p><b><i>Should we copy and paste .proto files in every repository where they are needed? </i></b><span style="font-weight: 400;">This is not a good idea and could be frustrating for the consuming teams. We should avoid any error-prone or manual activity in favor of a fully automated process. This will drive consistency and reduce toil.</span></p> <p><b><i>How will changes in .proto files impact clients?</i></b> <span style="font-weight: 400;">We  should implement a </span><a href="https://docs.microsoft.com/en-us/aspnet/core/grpc/versioning?view=aspnetcore-6.0"><span style="font-weight: 400;">versioning strategy </span></a><span style="font-weight: 400;">to support changes.</span></p> <p><b><i>How do we communicate changes to clients?</i></b> <span style="font-weight: 400;">We need a common place to share multiple versions with other teams and adopt a standard header to client  expectations, such as </span><a href="https://tools.ietf.org/id/draft-dalal-deprecation-header-01.html"><span style="font-weight: 400;">Deprecation</span></a><span style="font-weight: 400;"> and </span><a href="https://datatracker.ietf.org/doc/html/rfc8594"><span style="font-weight: 400;">Sunset</span></a><span style="font-weight: 400;">.</span></p> <h2><span style="font-weight: 400;">Our proposed solution</span></h2> <p><span style="font-weight: 400;">We will maintain protobuf files within the owning service’s repository to simplify ownership. The code owners are responsible for generating the needed packages for their clients. Their CI/CD pipeline will automatically generate the library code from the protobuf file for each target language.</span></p> <p><span style="font-weight: 400;">Packages will be published in a central place to be consumed by all client teams. Each package will be versioned for consistency and communication. Before deprecating and sunsetting any package version, all clients must  be notified and given enough time to upgrade.</span></p> <h3><span style="font-weight: 400;">Repository Structure</span></h3> <p><span style="font-weight: 400;">In our opinion, having a monorepo for all protobuf definitions would slow down the teams’ development cycles: each  modification to a Protobuf definition would require a PR to publish  the change in the monorepo, waiting for an approval  before  generating required  artifacts and distributing them to clients. Once the package was published, teams would have to update the package and publish a new version of their services. We need to keep the Protobuf files with  their owning service. </span></p> <h3><span style="font-weight: 400;">Project Structure</span></h3> <p><span style="font-weight: 400;">The project’s organization should  provide a clear distinction between the services that exist in the project and the underlying Protobuf version that the package is implementing. The </span><b><i>proto</i></b><span style="font-weight: 400;"> folder will hold the definition of each proto file with a correctly formed version using the </span><a href="https://developers.google.com/protocol-buffers/docs/proto3#packages"><span style="font-weight: 400;">package specifier</span></a><span style="font-weight: 400;">. The </span><b><i>service</i></b><span style="font-weight: 400;"> folder will hold the implementation of each gRPC service which is registered against the server. </span></p> <p><figure id="attachment_9199" aria-describedby="caption-attachment-9199" style="width: 1239px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9199 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.18.16-PM.png" alt="The proto folder will hold the definition of each proto file with a correctly formed version using the package specifier." width="1239" height="309" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.18.16-PM.png 1239w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.18.16-PM-300x75.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.18.16-PM-768x192.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.18.16-PM-1024x255.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9199" class="wp-caption-text">The proto folder will hold the definition of each proto file with a correctly formed version using the package specifier.</figcaption></figure></p> <p><span style="font-weight: 400;">This approach will allow us to publish a v2 version of our service with breaking change, while we continue supporting the v1 version. We should take into consideration the next points when we publish a new version of our service:</span></p> <ul> <li><span style="font-weight: 400;">Try to avoid breaking changes (Backward and forward compatibility)</span></li> <li><span style="font-weight: 400;">Do not change the version unless making breaking changes.</span></li> <li><span style="font-weight: 400;">Do change the version when making breaking changes.</span></li> </ul> <h2><span style="font-weight: 400;">Proto file validation</span></h2> <p><span style="font-weight: 400;">To make sure the proto files do not contain errors and to enforce good API design choices we recommend using </span><a href="https://github.com/bufbuild/buf"><span style="font-weight: 400;">Buf</span></a><span style="font-weight: 400;"> as a linter and a breaking change detector. It should be used on a daily basis as part of the development workflow, for example, by adding a pre-commit check to ensure our proto files do not contain any errors.</span></p> <p><span style="font-weight: 400;">Following our “reduce toil over automation” principle, we added a task in our CI/CD pipelines in CircleCI. A </span><a href="https://hub.docker.com/r/bufbuild/buf"><span style="font-weight: 400;">Docker image</span></a><span style="font-weight: 400;"> is available to add some steps for linting and breaking change detection. It helps us to ensure that we publish error-free packages:</span></p> <p><figure id="attachment_9219" aria-describedby="caption-attachment-9219" style="width: 1229px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9219 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.21-PM.png" alt="Following our “reduce toil over automation” principle, we added a task in our CI/CD pipelines in CircleCI." width="1229" height="654" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.21-PM.png 1229w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.21-PM-300x160.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.21-PM-768x409.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.21-PM-1024x545.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9219" class="wp-caption-text">Following our “reduce toil over automation” principle, we added a task in our CI/CD pipelines in CircleCI.</figcaption></figure></p> <p><span style="font-size: 1rem;"><br /> If a developer pushes breaking changes or changes with linter problems, our CI/CD pipelines in CircleCI will fail as can be  seen in the pictures below:</span></p> <p><figure id="attachment_9229" aria-describedby="caption-attachment-9229" style="width: 1253px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9229 size-full" style="font-size: 1rem;" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.37-PM.png" alt="breaking changes or changes with linter problems, our CI/CD pipelines in CircleCI will fail" width="1253" height="442" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.37-PM.png 1253w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.37-PM-300x106.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.37-PM-768x271.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.37-PM-1024x361.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9229" class="wp-caption-text">Breaking changes or changes with linter problems, our CI/CD pipelines in CircleCI will fail.</figcaption></figure></p> <h3><span style="font-weight: 400;">Linter problems</span></h3> <p><figure id="attachment_9239" aria-describedby="caption-attachment-9239" style="width: 1264px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9239 size-full" style="font-size: 1rem;" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.46-PM.png" alt="Example Linter problems" width="1264" height="695" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.46-PM.png 1264w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.46-PM-300x165.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.46-PM-768x422.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.46-PM-1024x563.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9239" class="wp-caption-text">Example Linter problems</figcaption></figure></p> <h3><span style="font-weight: 400;">Breaking changes</span></h3> <p><figure id="attachment_9249" aria-describedby="caption-attachment-9249" style="width: 1256px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9249 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.54-PM.png" alt="Example Breaking changes" width="1256" height="482" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.54-PM.png 1256w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.54-PM-300x115.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.54-PM-768x295.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.20.54-PM-1024x393.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9249" class="wp-caption-text">Example breaking changes</figcaption></figure></p> <h2><span style="font-weight: 400;">Versioning packages</span></h2> <p><span style="font-weight: 400;">Another challenge is building and versioning artifacts from the protobuf file-generated code. We selected </span><a href="https://semver.org/"><span style="font-weight: 400;">Semantic Versioning</span></a><span style="font-weight: 400;"> as a way to publish and release packages’ versions.</span></p> <p><span style="font-weight: 400;">The package name should reflect the service name and follow the conventions established by the language, platform, framework and community.</span></p> <h2><span style="font-weight: 400;">Generating code for libraries</span></h2> <p><span style="font-weight: 400;">We have set up an automated process in CircleCI to generate code for libraries. Once a proto file is changed and tagged, CircleCI detects the changes and begins generating the code from the proto file.</span></p> <p><span style="font-weight: 400;">We compile it using </span><a href="https://grpc.io/docs/protoc-installation/"><span style="font-weight: 400;">protoc</span></a><span style="font-weight: 400;">. To avoid the burden of installing it, we use a </span><a href="https://www.docker.com/"><span style="font-weight: 400;">Docker</span></a><span style="font-weight: 400;"> image that contains it. This facilitates our local development as well as CI/CD pipelines. Here is the CircleCI configurations:</span></p> <p><figure id="attachment_9259" aria-describedby="caption-attachment-9259" style="width: 1236px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9259 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.11-PM.png" alt="We compile it using protoc. To avoid the burden of installing it, we use a Docker image that contains it." width="1236" height="519" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.11-PM.png 1236w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.11-PM-300x126.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.11-PM-768x322.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.11-PM-1024x430.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9259" class="wp-caption-text">We compile it using protoc. To avoid the burden of installing it, we use a Docker image that contains it.</figcaption></figure></p> <p><span style="font-weight: 400;">In the previous example, we are generating code for python but it can also be generated for Java, Ruby, Go, Node, C#, etc.</span></p> <p><span style="font-weight: 400;">Once code is generated and persisted into a </span><a href="https://circleci.com/docs/2.0/workspaces/"><span style="font-weight: 400;">CircleCI workspace</span></a><span style="font-weight: 400;"> it’s time to publish our package.</span></p> <h2><span style="font-weight: 400;">Publishing packages</span></h2> <p><span style="font-weight: 400;">This process could be overwhelming for teams if they had to figure out how to package and publish each artifact in all supported languages in our </span><a href="https://www.eventbrite.com/engineering/writing-our-golden-path/"><span style="font-weight: 400;">Golden Path</span></a><span style="font-weight: 400;">. For this reason we took the same approach as </span><a href="https://github.com/namely/docker-protoc"><span style="font-weight: 400;">docker-protoc</span></a><span style="font-weight: 400;"> and we dockerized a tool that we developed called </span><b>protop</b><span style="font-weight: 400;">.</span></p> <p><span style="font-weight: 400;">Protop is a simple Python project that combines </span><a href="https://typer.tiangolo.com/"><span style="font-weight: 400;">typer</span></a><span style="font-weight: 400;"> and </span><a href="https://github.com/cookiecutter/cookiecutter"><span style="font-weight: 400;">cookiecutter</span></a><span style="font-weight: 400;"> to provide us a way to package the code into a library for each language. At the moment it only supports </span><a href="https://pypi.org/"><span style="font-weight: 400;">PyPI</span></a><span style="font-weight: 400;"> using </span><a href="https://pypi.org/project/twine/"><span style="font-weight: 400;">Twine</span></a><span style="font-weight: 400;"> because our main codebase of consumers are in Python, but we are planning to addGradle support soon.</span></p> <p><span style="font-weight: 400;">The use of protop is very similar to </span><a href="https://github.com/namely/docker-protoc"><span style="font-weight: 400;">docker-protoc</span></a><span style="font-weight: 400;">. We published a dockerized version of protop to an </span><a href="https://aws.amazon.com/ecr/"><span style="font-weight: 400;">AWS Elastic Container Registry</span></a><span style="font-weight: 400;"> to allow teams to use it in their CI/CD pipelines in CircleCI:</span></p> <p><figure id="attachment_9269" aria-describedby="caption-attachment-9269" style="width: 1237px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9269 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.46.30-PM.png" alt="We published a dockerized version of protop to an AWS Elastic Container Registry to allow teams to use it in their CI/CD pipelines in CircleCI" width="1237" height="845" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.46.30-PM.png 1237w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.46.30-PM-300x205.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.46.30-PM-768x525.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.46.30-PM-1024x699.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9269" class="wp-caption-text">We published a dockerized version of protop to an AWS Elastic Container Registry to allow teams to use it in their CI/CD pipelines in CircleCI</figcaption></figure></p> <p><span style="font-weight: 400;">At Eventbrite we use </span><a style="font-size: 1rem;" href="https://aws.amazon.com/codeartifact/">AWS CodeArtifact</a><span style="font-weight: 400;">  in order to store other internal libraries so we decided to re-use it to store our gRPC service libraries. You can see a diagram of the overall process below.</span></p> <p><figure id="attachment_9279" aria-describedby="caption-attachment-9279" style="width: 1067px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9279 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.30-PM.png" alt=" AWS CodeArtifact  stores both internal libraries and our gRPC service libraries." width="1067" height="986" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.30-PM.png 1067w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.30-PM-300x277.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.30-PM-768x710.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.30-PM-1024x946.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9279" class="wp-caption-text">AWS CodeArtifact  stores both internal libraries and our gRPC service libraries.</figcaption></figure></p> <p><span style="font-weight: 400;">This AWS CodeArtifact repository should be shared by all teams in order to have only one common place to find those packages instead of having to ask each team what repository they have stored their packages in and having lots of keys to access them.</span></p> <p><span style="font-weight: 400;">The teams that want to consume those packages should configure their CI/CD pipelines to pull the libraries down from AWS CodeArtifact when their services are built.</span></p> <p><span style="font-weight: 400;">This process will help us reduce the amount of time spent in service integration without diminishing the teams’ code ownership..</span></p> <h2><span style="font-weight: 400;">Using the packages</span></h2> <p><span style="font-weight: 400;">The last step is to use our package. With the package uploaded to </span><a href="https://aws.amazon.com/codeartifact/"><span style="font-weight: 400;">AWS CodeArtifact</span></a><span style="font-weight: 400;">, we need to update our </span><a href="https://github.com/pypa/pipfile"><span style="font-weight: 400;">Pipfile</span></a><span style="font-weight: 400;">:</span></p> <p><figure id="attachment_9289" aria-describedby="caption-attachment-9289" style="width: 1233px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9289 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.49.00-PM.png" alt="Updated PIp File to use the artifact." width="1233" height="344" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.49.00-PM.png 1233w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.49.00-PM-300x84.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.49.00-PM-768x214.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.49.00-PM-1024x286.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9289" class="wp-caption-text">Updated PIP File to use the artifact.</figcaption></figure></p> <p><span style="font-weight: 400;">or </span><a href="https://www.jetbrains.com/help/pycharm/managing-dependencies.html#create-requirements"><span style="font-weight: 400;">requirements txt</span></a></p> <p><figure id="attachment_9299" aria-describedby="caption-attachment-9299" style="width: 1233px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="wp-image-9299 size-full" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.48-PM.png" alt="Alternative way of using Protobuf files." width="1233" height="56" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.48-PM.png 1233w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.48-PM-300x14.png 300w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.48-PM-768x35.png 768w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2022/05/Screen-Shot-2022-05-02-at-12.21.48-PM-1024x47.png 1024w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption id="caption-attachment-9299" class="wp-caption-text">Alternative way of using Protobuf files.</figcaption></figure></p> <h2><span style="font-weight: 400;">Conclusion</span></h2> <p><span style="font-weight: 400;">We started out by defining the challenges of managing Protobuf definitions at Eventbrite, explaining the key questions about where to store these definitions, how to manage changes and how to communicate those changes. We’ve also explained the repository and project structure.</span></p> <p><span style="font-weight: 400;">Then, we proceed to cover protobuf validation using </span><a href="https://github.com/bufbuild/buf"><span style="font-weight: 400;">Buf</span></a><span style="font-weight: 400;"> as a linter and a breaking change detector in our CI/CD pipelines and how to version using </span><a href="https://semver.org/"><span style="font-weight: 400;">Semantic Versioning</span></a><span style="font-weight: 400;"> as a way to publish and release packages’ versions.</span></p> <p><span style="font-weight: 400;">After that, we’ve turned out to focus on how to generate, publish and consume our libraries as a kind of SDK for the service&#8217;s domain allowing other teams to consume gRPC services in a simple way..</span></p> <p><span style="font-weight: 400;">But of course, this is the first iteration of the project and we are already planning actions to be more efficient and further reduce toil over automation. For example, we are working on generating the packages’ version automatically using something similar to </span><a href="https://github.com/semantic-release/semantic-release"><span style="font-weight: 400;">Semantic Release</span></a><span style="font-weight: 400;"> to avoid teams having to update the package version manually and therefore avoiding error-prone interactions. </span></p> <p><span style="font-weight: 400;">To summarize, if you want to drastically reduce the time that teams waste on service integration avoiding a lot of manual errors, consider automating as much as you can the process of generating, publishing and consuming your gRPC client libraries.</span></p> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-8769" class="post-8769 post type-post status-publish format-standard hentry category-architecture category-career-growth category-datastores category-management category-scalability tag-architecture tag-devops"> <header class="entry-header"> <h3 class="entry-title heading-large"><a href="https://www.eventbrite.com/engineering/reflecting-on-the-eventbrite-journey-from-centralized-ops-to-devops/" rel="bookmark">Reflecting on Eventbrite’s Journey From Centralized Ops to DevOps</a></h3> <div class="post-thumbnail"> <a href="https://www.eventbrite.com/engineering/reflecting-on-the-eventbrite-journey-from-centralized-ops-to-devops/"> </a> </div><!-- .post-thumbnail --> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/reflecting-on-the-eventbrite-journey-from-centralized-ops-to-devops/" rel="bookmark"><time class="entry-date published" datetime="2022-01-21T14:45:31-08:00">January 21, 2022</time><time class="updated" datetime="2022-01-21T14:46:57-08:00">January 21, 2022</time></a></div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p><span style="font-weight: 400;">Once a scrappy startup, Eventbrite has quickly grown into </span><i><span style="font-weight: 400;">the</span></i><span style="font-weight: 400;"> market leader for live event ticketing globally. Our technical stack changed during the first few years, but as with most things that reach production, pieces and patterns lingered. </span></p> <p><span style="font-weight: 400;">Over the years, we leaned heavily into a Django, Python, MySQL stack, and our monolith grew. We changed how our monolith was deployed and scaled as we went into the AWS cloud as an early adopter. This entailed building internal tooling and processes to solve specific problems we were facing, and doubling down on our internal tooling while the cloud matured around us. </span></p> <h2>Keeping up with traffic bursts from high-demand events</h2> <p><span style="font-weight: 400;">Part of the fun and challenge of being a primary ticketing company is handling burst traffic from high traffic on-sales — these are high-demand events that generate traffic spikes when tickets are released to purchase at a specific time. Creators (how we refer to folks that host events) will often gate traffic externally, and post a direct link to an Eventbrite listing on a social network or their own websites. Traffic builds on these sites while customers wait for a link to be posted. The result is </span><i><span style="font-weight: 400;">hundreds of thousands of customers hitting our site at once</span></i><span style="font-weight: 400;">. </span></p> <p><span style="font-weight: 400;">Ten-plus years ago, this was incredibly difficult to solve, and it’s still a fun challenge from a speed of scale and cost perspective. Ultimately, challenges around the reliability of our monolithic architecture led to us investing in specialized engineering teams to help manually scale the site up during these traffic bursts as well as address the day-to-day maintenance and upkeep of the infrastructure we were running. </span></p> <h2>A monolithic architecture isn’t a bad thing — it just means there are tradeoffs<span style="font-weight: 400;"> </span></h2> <p><span style="font-weight: 400;">On one hand, our monolithic setup allowed us to move fast. Having some of Django’s core contributors helped us solve complex industry problems, such as high-volume on-sales in which small numbers of tickets go on sale to large numbers of customers. On the other hand, as we and our platform’s features grew, things became unwieldy, and we centralized our production and deployment maintenance in response to site incidents and bug triage. </span></p> <p><span style="font-weight: 400;">This led to us trying to break up the monolith. The result? Things got worse because we didn’t address the data layer and ended up with mini Django monoliths, which we incorrectly called services.</span></p> <h2>The decision to move from an Ops model to a DevOps model, and the hurdles along the way</h2> <p><span style="font-weight: 400;">Enter our </span><a href="https://www.eventbrite.com/engineering/how-we-created-our-3-year-technical-vision/" target="_blank" rel="noopener"><span style="font-weight: 400;">three-year technical vision</span></a><span style="font-weight: 400;">. In order to address our slowing developer velocity and improve our reliability, performance, and scale, we made an engineering-wide declaration to move away from an Ops model — in which a centralized team had all the keys to our infrastructure and our deployments— to a DevOps model in which each team had full ownership. </span></p> <p><span style="font-weight: 400;">An initial hurdle we had to jump over was a process hurdle. In order for teams to take any ownership, they’d have to be on call 24&#215;7 for the services and code they owned. We had a small number of teams with production access that were on call, but the vast majority of our teams were not. This was an important moment in our ownership journey. And our engineering teams had many questions about the implications of what was not only a cultural but also a process change.</span></p> <p><span style="font-weight: 400;">There are many technical hurdles to providing team-level ownership, and it’s tempting to get drawn into a “boil-the-ocean” moment and throw away all the historic learnings and business logic we developed over our history. Our primary building block towards team autonomy was leveraging a multi-AWS sub-account strategy. Using Terraform, we were able to build an account vending system allowing teams to design clear walls between their workloads, frontends, and services. With these walls in place each team had better control and visibility into the code they owned. </span></p> <h2>Technical debt, generally, is a complicated ball of yarn to unwind</h2> <p><span style="font-weight: 400;">We had many centralized EC2-based data clusters: MySQL, Redis, Memcache, ElasticSearch, Kafka, etc. Migrating these to managed versions — and the transfer of ownership between our legacy centralized ownership directly to teams — required a high degree of cross-team coordination and focused team capacity. </span></p> <p><span style="font-weight: 400;">As an example, the migration of our primary MySQL cluster to Aurora required 60 engineers during the off-hours writer cutover — they  represented all of our development teams. The effort towards the decentralization of our data is leading us to develop full-featured infrastructure as code building blocks that teams can pull off the shelf to leverage the full capabilities of best-in-class managed data services.</span></p> <p><span style="font-weight: 400;">Our systems powering our frontend as well as our backend services are process-wise similar to our data-ownership journey. We have examples of innovation around serverless compute patterns and new architectural approaches to address scale and reliability. We’re making big bets on some of our largest and most-impactful services — two of which still live as libraries in our core monolith. The learnings that are accrued through these efforts will power the second and third year of our three-year tech vision journey. </span></p> <h2>The impact thus far, with more unlocks to come</h2> <p><span style="font-weight: 400;">By now, you&#8217;re probably realizing that at least some of our teams were shocked at the amount of change happening as their ownership responsibilities increased. We were confident that this short-term pain was worth it. After all, our teams were demanding this through direct feedback in our dev and culture surveys. </span></p> <p><span style="font-weight: 400;">The prize for us on this journey is customer value delivered through increased team velocity. While our monolithic architecture — both on the code and data sides of the house -— got us to where we are today, teams were not happy with their ability to bring change and improvements to things that they owned. This was frustrating for everyone involved, and the gold at the end of the rainbow for us is that teams can make fundamental changes with modern tools and processes. </span></p> <p><span style="font-weight: 400;">In the first year of our three-year technical vision big changes in ownership have been unlocked. As an example, we have migrated to Aurora where teams have ownership of their data. We’ve also provided direct team-level ownership of teams CI pipelines, improved our overall code coverage for testing, provided team autonomy for feature flag releases, and started re-architecting our two largest tier-1 services. It’s exciting to see new sets of challenges arise along the way — knowing these hurdles also unveil opportunities. </span></p> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-8389" class="post-8389 post type-post status-publish format-standard hentry category-analytics category-data category-datastores category-data-strategy"> <header class="entry-header"> <h3 class="entry-title heading-large"><a href="https://www.eventbrite.com/engineering/creating-the-eventbrite-data-vision/" rel="bookmark">Crafting Eventbrite&#8217;s Data Vision</a></h3> <div class="post-thumbnail"> <a href="https://www.eventbrite.com/engineering/creating-the-eventbrite-data-vision/"> </a> </div><!-- .post-thumbnail --> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/creating-the-eventbrite-data-vision/" rel="bookmark"><time class="entry-date published" datetime="2021-12-09T18:46:24-08:00">December 9, 2021</time><time class="updated" datetime="2021-12-10T08:10:46-08:00">December 10, 2021</time></a></div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p><span style="font-weight: 400;">Data-driven decisions are the irrefutable holy grail for any company, especially one like Eventbrite, whose mission is to connect the world through live experiences. </span></p> <p><span style="font-weight: 400;">I joined the Briteland to lead the Data Org, merging data-platform engineering, analytics engineering, product analytics, strategic insights and data science under one umbrella with a North Star of leveraging our scale and driving actionable insights from data.</span></p> <p><span style="font-weight: 400;">When I first met fellow Britelings earlier this year, what immediately won me over was their infectious enthusiasm about the company’s mission, potential for impact — and, importantly, that data is a critical strategic asset to realizing Eventbrite&#8217;s vision. </span></p> <h2>Challenges and Opportunities</h2> <p><span style="font-weight: 400;">The </span><i><span style="font-weight: 400;">Data Nerd</span></i><span style="font-weight: 400;"> in me couldn’t wait to uncover insights from this rich trove of data: social dynamics and the evolution of live experiences during and post the pandemic, regional microtrends, correlation to vaccinations rates, and so much more. </span></p> <p><span style="font-weight: 400;">However, to first get grounded in reality, I’ve had to play a couple of different roles. As a </span><i><span style="font-weight: 400;">Data</span></i><span style="font-weight: 400;"><em> Lobbyist</em>,</span><i> </i><span style="font-weight: 400;">I’ve been encouraging everyone, from our leaders to our engineers, to seek out data to guide their decisions. As a </span><i><span style="font-weight: 400;">Data Therapist</span></i><span style="font-weight: 400;">, I listen and learn from Britelings across all functions about the obstacles they encounter in gaining insights from data. Britelings’ current pain points broadly fall into three buckets: <strong>people</strong>, <strong>process</strong>, and <strong>technology</strong>.</span></p> <h4>People</h4> <p><span style="font-weight: 400;">Britelings are not aware of what data exists where, and how to start self-serving, especially when they may not have access to get started. As a result, “quick answers” aren’t quick enough, and thorough answers are even more time-consuming, especially when an analyst needs to spare cycles on techniques, tooling, or data semantics. </span></p> <p><span style="font-weight: 400;">In addition, development teams are currently dependent on the data engineering team to aggregate and provide data for use in products. This does not align with our technical vision to have each development team own their solution end-to-end, including design, code, quality, deployment, monitoring, data, and infrastructure.</span></p> <p><i><span style="font-weight: 400;">Focus areas: build data culture, remove knowledge silos</span></i></p> <h4>Process</h4> <p><span style="font-weight: 400;">There are multiple sources of truth (internal systems, data marts, etc.) that do not always reconcile. There are also several holes between data consumers and data producers in which context gets lost, and there&#8217;s a lack of standardized processes to define and update metrics. </span></p> <p><span style="font-weight: 400;">Added to this, various manual processes are used for business-critical reporting due to legacy pipelines, data gaps, and incomplete context, causing transformation logic to be siloed with key employees rather than codified systematically with disciplined documentation.</span></p> <p><i><span style="font-weight: 400;">Quick wins: align stakeholders, build end-to-end runbooks, alert proactively</span></i></p> <h4>Technology</h4> <p><span style="font-weight: 400;">Dated data infrastructure and stale models have been challenging to maintain and use. With insufficient isolation between production, development, and testing, some production pipelines have emerged as bottlenecks hindering quick iteration. </span></p> <p><span style="font-weight: 400;">In addition, in the absence of consistent tooling and guidelines for getting data instrumented in products and integrated into existing pipelines, there are gaps in data coverage and quality that need to be addressed.<br /> </span></p> <p><i><span style="font-weight: 400;">Slow down to speed up: modernize infrastructure, implement SDLC for data</span></i></p> <p><span style="font-weight: 400;">These challenges are certainly not unique to Eventbrite; it’s an operational reality for businesses in the modern world. As a </span><i><span style="font-weight: 400;">Data Leader</span></i><span style="font-weight: 400;">, it’s heartening to know that Britelings are eager to lift barriers and invest in opportunities that deliver tangible value to our customers!</span></p> <h2>Goals</h2> <p><span style="font-weight: 400;">With a better understanding of where the gaps are across people, process, and technology, we set the following five goals:</span></p> <ol> <li><span style="font-weight: 400;">Provide a single source of truth with high-quality data for operational and financial reporting needs.  </span></li> <li><span style="font-weight: 400;">Provide tooling, training, and automation for Britelings to make informed decisions autonomously. </span></li> <li><span style="font-weight: 400;">Provide reliable, resilient, scalable, and cost-effective data infrastructure.</span></li> <li><span style="font-weight: 400;">Make data more actionable to internal stakeholders, enabling a 360-degree perspective for strategic decisions.</span></li> <li><span style="font-weight: 400;">Make data actionable, insightful, and valuable to customers in-product — help creators grow their audience, help attendees find relevant events, and make the product more self-service. </span></li> </ol> <h2>Tenets</h2> <p><span style="font-weight: 400;">To achieve these goals, we converged on tenets that would guide our execution, especially when confronted with tradeoffs.</span></p> <ul> <li style="font-weight: 400;" aria-level="1"><b>Democratization over gatekeeping: </b><span style="font-weight: 400;">We favor making data accessible to people (Britelings and Customers) and easier to create/collect more broadly to maximize creativity and value from data — but only within the boundaries of maintaining security and compliance.</span></li> <li style="font-weight: 400;" aria-level="1"><b>Self-service over full-service: </b><span style="font-weight: 400;">We will provide tools and consultation for people to better self-serve on data and not rely solely on a centralized team for insights.</span></li> <li style="font-weight: 400;" aria-level="1"><b>Agility over uniformity: </b><span style="font-weight: 400;">We believe in not blocking teams from their deliverables if they have a “good enough” option to run with sooner, and iteratively improve based on feedback. We will aim for developer autonomy.</span></li> <li style="font-weight: 400;" aria-level="1"><b>Connect and enrich over clone and customize: </b><span style="font-weight: 400;">We prefer to create and enrich modular datasets with additional context, annotations, information for consistent interpretation and use instead of making multiple copies that may eventually diverge and cause inaccuracies or confusion.</span></li> <li style="font-weight: 400;" aria-level="1"><b>Comprehensive accuracy over partial freshness: </b><span style="font-weight: 400;">We will prioritize having correct and complete information as of a (recent) point in time over up-to-date information that has not been vetted or reconciled, unless there is a use-case that demands otherwise.</span></li> </ul> <h2>Vision</h2> <p><span style="font-weight: 400;">With goals and ground rules established, we realize that the data team’s mission is to </span><i><span style="font-weight: 400;">enable Creators, Attendees, and Britelings to self-serve on high-quality data at scale to derive actionable insights that drive business impact. </span></i></p> <p><span style="font-weight: 400;">It is our vision that: </span></p> <ul> <li style="font-weight: 400;" aria-level="1"><i><span style="font-weight: 400;">Creators</span></i><span style="font-weight: 400;"> obtain actionable insights to build their audience, increase ticket purchases, manage their events, and build loyalty amongst their attendees.</span></li> <li style="font-weight: 400;" aria-level="1"><i><span style="font-weight: 400;">Attendees</span></i><span style="font-weight: 400;"> find interesting and relevant events from creators they trust. </span></li> <li style="font-weight: 400;" aria-level="1"><i><span style="font-weight: 400;">Britelings</span></i><span style="font-weight: 400;"> have accurate, timely, and actionable insights for operating the business, building better products, and delighting our creators and attendees.</span></li> </ul> <p><span style="font-weight: 400;">In upcoming posts, we will talk about the Data Strategy and our plans to deliver on this vision.</span><span style="font-weight: 400;"> </span></p> </div><!-- .entry-content --> </article><!-- #post-## --> <article id="post-8189" class="post-8189 post type-post status-publish format-image hentry category-architecture category-front-end category-performance category-scalability post_format-post-format-image"> <header class="entry-header"> <div class="entry-meta"><span class="screen-reader-text">Posted on</span> <a href="https://www.eventbrite.com/engineering/creating-the-3-year-frontend-strategy/" rel="bookmark"><time class="entry-date published" datetime="2021-11-01T09:00:56-07:00">November 1, 2021</time><time class="updated" datetime="2021-10-05T15:13:36-07:00">October 5, 2021</time></a></div><!-- .entry-meta --><h3 class="entry-title"><a href="https://www.eventbrite.com/engineering/creating-the-3-year-frontend-strategy/" rel="bookmark">Creating the 3 Year Frontend Strategy</a></h3> </header><!-- .entry-header --> <div class="entry-content"> <p><span style="font-weight: 400;">Last post we talked about <a href="https://www.eventbrite.com/engineering/creating-a-3-year-frontend-vision/">Developing the 3 Year Frontend Vision</a>, in this post we will go into how that vision, the tenets, requirements, and challenges shaped the Strategy moving forward.</span></p> <p><span style="font-weight: 400;">One of the key themes in Eventbrite since I joined is DevOps, moving ownership from a single team who has been responsible for ops and distributing that responsibility to each individual team. To give them ownership over decisions, infrastructure, and to control their own destiny. The first step in defining the Strategy was to put together what a Technical Strategy is, and the foundation for that strategy.</span></p> <h2><span style="font-weight: 400;">Technical Strategy</span></h2> <p><span style="font-weight: 400;">The overall Technical Strategy is based on availability and ownership. Starting with the way we build our services and frontends, to the way we deploy and serve assets to our customers. The architecture is designed to reduce the blast radius of errors, increase our uptime, and give each team as much control over their space as possible.</span></p> <h3><span style="font-weight: 400;">Availability</span></h3> <p><span style="font-weight: 400;">Moving forward we will achieve High Availability (HA), in which our frontends and systems are resilient to faults and traffic, and will operate continuously without human intervention. In order to achieve HA, we will utilize Managed AWS Services or redundant fault tolerant software, and by utilizing content delivery networks (CDN) to increase our performance and resilience by putting our code as close to the customer as possible. We will ensure that all aspects of the system are tested, fault tolerant, and resilient, and that both the client-side and server-side gracefully degrade when downstream services fail.</span></p> <h3><span style="font-weight: 400;">Ownership</span></h3> <p><span style="font-weight: 400;">DevOps combines the traditional software development by one team and operations and infrastructure by another into a single team responsible for the full lifecycle of development and infrastructure management. This combination enables organizations to deliver applications at a higher velocity, evolving and improving their products at a faster pace than traditional split teams. The goal of DevOps is to shift the ownership of decision making from the management structure to the developers, improve processes, and remove unproductive barriers that have been put in place over the years.</span></p> <h2><span style="font-weight: 400;">Frontend</span></h2> <p><img loading="lazy" decoding="async" class="wp-image-8199 alignleft" title="Frontend boundary" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2021/09/frontend-188x300.png" alt="" width="235" height="375" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2021/09/frontend-188x300.png 188w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2021/09/frontend.png 351w" sizes="(max-width: 235px) 100vw, 235px" /></p> <p><span style="font-weight: 400;">Once we had the foundation of the strategy defined, it was time to define the scope. To understand how to develop a strategy, or to even define one, we need to understand what makes up a “frontend”. In our case, the Frontend is everything from the backend service api calls to the customer. Because of this, we need to design a solution that allows for code to be run in a browser, on a server, service calls from a browser. Once you define the surface area of the solution, it becomes apparent that the scope and complexity of this problem is quickly compounding.</span></p> <h2><span style="font-weight: 400;">High Level Architecture</span></h2> <p><img loading="lazy" decoding="async" class="size-medium wp-image-8209 alignright" style="font-size: 1rem;" src="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2021/09/highlevelarch-232x300.png" alt="" width="232" height="300" srcset="https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2021/09/highlevelarch-232x300.png 232w, https://www.eventbrite.com/engineering/wp-content/uploads/engineering/2021/09/highlevelarch.png 563w" sizes="(max-width: 232px) 100vw, 232px" /></p> <p><span style="font-weight: 400;">We need to define an architecture for everything above the red line in the above graphic. In order to simplify the design, I broke this down into three main areas; The UI Layer consisting of a micro-frontend framework with team built </span></p> <p><span style="font-weight: 400;">Custom Components, a shared Content Delivery Network (CDN) to front all customer facing pages, and a deployable set of bundled software that we code named Oberon, including a UI Rendering Service and a Backend-For-Frontend.</span></p> <h3><span style="font-weight: 400;">UI Layer</span></h3> <p><span style="font-weight: 400;">The UI leverages the micro-frontend architecture and modern web framework best practices to build frontends that leverage browser specifications while being resilient and team owned.</span></p> <h4>Micro-Frontend</h4> <p><span style="font-weight: 400;">When first approaching the micro-frontend architecture I realized that there is no clear definition of what a micro-frontend is. </span></p> <p><a href="https://martinfowler.com/articles/micro-frontends.html"><span style="font-weight: 400;">Martin Fowler</span></a><span style="font-weight: 400;"> has a very high level definition which he states as </span></p> <blockquote><p>“An architectural style where independently deliverable frontend applications are composed into a greater whole”.</p></blockquote> <p><span style="font-weight: 400;"><a href="https://www.xenonstack.com/insights/micro-frontend-architecture">Xenon Stack</a> describes a Micro-frontend as</span></p> <blockquote><p>“a Microservice Testing approach to front-end web development.”</p></blockquote> <p><span style="font-weight: 400;">Reading through the many opinions and definitions, I felt it was necessary to get a clearer understanding, and for everyone to agree what a micro-frontend architecture is. I worked with a couple of other Frontend Engineers to put together the following definition for a Micro-Frontend.</span></p> <h5><span style="font-weight: 400;">Definition</span></h5> <p><span style="font-weight: 400;">A Micro-Frontend is an Architecture for building reusable and shareable frontends. They are independently deployable, composable frontends made up of components which can stand on their own or be combined with other components to form a cohesive user experience. This architecture is generally supported by hosting a parent application which dynamically slots in child components. Components within a micro-frontend should not explicitly communicate with external entities, but instead publish and subscribe to state updates to maintain loose coupling. </span></p> <p><span style="font-weight: 400;">Micro-frontends are inspired by the move to microservices on the backend, bringing the same level of ownership and team independent development and delivery to the frontend.</span></p> <h4>Self-Contained Components</h4> <p><span style="font-weight: 400;">In order to avoid frontends that over time inadvertently tightly couple themselves and create fragile un-reusable components, we must build components that are encapsulated, isolated, and able to render without the requirement of any other component on the page. </span></p> <h4>Component Rendering Pipeline</h4> <p><span style="font-weight: 400;">The Component Rendering pipeline renders components to the customer while the framework defines a set of Interfaces, Application Context, and a predictable state container for use across all of the rendering components.</span></p> <h4>State Management</h4> <p><span style="font-weight: 400;">State management is responsible for maintaining the application state, inter-component communication and API calls. State updates are unidirectional; updates trigger state changes which in turn invoke the appropriate components so they can act on the changes. </span></p> <h3><span style="font-weight: 400;">Content Delivery Network</span></h3> <p><span style="font-weight: 400;">Our current architecture has resilience issues, where one portion of the site may become slow or unresponsive and that has a direct impact on the rest of the domain, and in many cases cause an overall site availability issue. In order to get around some of this issue, we add a CDN at the ingress of our call stack. Every downstream frontend rendering will contain Cache-Control headers, in order to control the caching of assets and pages in the CDN. During a site availability issue, the rendering fleet may increase the cache control header, caching for small amounts of time (60 seconds &#8211; 5 minutes max), for pages that don’t require dynamic rendering, or customer content. Thus taking load off the fleet and increasing it’s resource availability for other areas.</span></p> <h3><span style="font-weight: 400;">Oberon</span></h3> <p><span style="font-weight: 400;">Oberon is a collection of software and Infrastructure-as-Code (IaC) that enables teams to set up frontends quickly and to get in front of customers faster. It includes a configurable Gateway pre-configured for authentication as needed, a UI Rendering Service to server-side render UI’s, UI Asset Server to serve client side assets, and a stubbed out Backend-For-Frontend. </span></p> <h4><span style="font-weight: 400;">Server Side UI Rendering Service</span></h4> <p><span style="font-weight: 400;">The UI Rendering Service defines a runtime environment for rendering applications, their components, and is responsible for serving pages to customers. The service maps incoming requests to applications and pages, gathers dependency bundles, and renders the layout to the customer. Oberon will leverage the traffic absorbing nature of a CDN with the scaling of a full serverless architecture. </span></p> <h4><span style="font-weight: 400;">Backend-For-Frontends</span><span style="font-weight: 400;"> (BFF)</span></h4> <p><span style="font-weight: 400;">A BFF is part of the application layer, bridging the user experience and adding an abstraction layer over the backend microservices. This abstraction layer fills a gap that is inherent in the microservice architecture, where microservices must compete to be as generic as possible while the frontends need to be customer driven.  </span></p> <p><span style="font-weight: 400;">BFFs are optimized for each specific user interface, resulting in a smaller, less complex, and faster than generic backend, allowing the frontend code to 1) limit over-requesting on the client, 2) to be simpler, and 3) see a unified version of the backend data. Each interface team will have a BFF, allowing them autonomy to control their own interface calls, giving them the ability to choose their own languages and deploy as early or as often as they would like.</span></p> <h3>Next Steps.</h3> <p><span style="font-weight: 400;">Now that we’ve published the 3 Year Frontend Strategy, the hard work begins. Over the next few months we will be defining the low level architecture of Oberon, and working on a Proof Of Concept that teams can start to leverage in early 2022. </span></p> </div><!-- .entry-content --> </article><!-- #post-## --> <nav class="navigation pagination" aria-label="Posts"> <h2 class="screen-reader-text">Posts navigation</h2> <div class="nav-links"><span aria-current="page" class="page-numbers current"><span class="meta-nav screen-reader-text">Page </span>1</span> <a class="page-numbers" href="https://www.eventbrite.com/engineering/page/2/"><span class="meta-nav screen-reader-text">Page </span>2</a> <span class="page-numbers dots">&hellip;</span> <a class="page-numbers" href="https://www.eventbrite.com/engineering/page/17/"><span class="meta-nav screen-reader-text">Page </span>17</a> <a class="next page-numbers" href="https://www.eventbrite.com/engineering/page/2/"><span class="screen-reader-text">Next page</span><svg class="icon icon-arrow-right" aria-hidden="true" role="img"> <use href="#icon-arrow-right" xlink:href="#icon-arrow-right"></use> </svg></a></div> </nav> </main><!-- #main --> </div><!-- #primary --> <aside id="secondary" class="widget-area" role="complementary"> <section id="search-4" class="widget widget_search"> <form role="search" method="get" class="search-form" action="https://www.eventbrite.com/engineering/"> <label for="search-form-67465a0c6549a"> <span class="screen-reader-text">Search for:</span> </label> <input type="search" id="search-form-67465a0c6549a" class="search-field" placeholder="Search &hellip;" value="" name="s" /> <button type="submit" class="search-submit"><svg class="icon icon-search" aria-hidden="true" role="img"> <use href="#icon-search" xlink:href="#icon-search"></use> </svg><span class="screen-reader-text">Search</span></button> </form> </section><section id="text-4" class="widget widget_text"> <div class="textwidget"><h4 class="text-center">We're Hiring!</h4> <p class="text-center"><a href="https://www.eventbritecareers.com/home?lever-origin=applied&lever-source%5B%5D=EngBlog" class="btn btn--large">Join Our Team</a></p></div> </section><section id="authors-2" class="widget widget_authors"><h2 class="widget-title">Our Writing Team</h2><ul><li><a href="https://www.eventbrite.com/engineering/author/archana/"><strong>Archana Ganapathi</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/creating-the-eventbrite-data-vision/" title="Crafting Eventbrite&#8217;s Data Vision">Crafting Eventbrite&#8217;s Data Vision</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/ariel-elias/"><strong>Ariel Elias</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/desde-adentro-cronica-de-un-drink-up/" title="Desde adentro (Crónica de un drink up)">Desde adentro (Crónica de un drink up)</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/asmelser/"><strong>Andrew Smelser</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/8-simple-tips-for-better-communication-with-customer-facing-teams/" title="8 Simple Tips for better Communication with Customer-Facing Teams">8 Simple Tips for better Communication with Customer-Facing Teams</a></li><li><a href="https://www.eventbrite.com/engineering/rethinking-quality-engineers/" title="Rethinking quality and the engineers who protect it">Rethinking quality and the engineers who protect it</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/bartek-ogryczak/"><strong>Bartek Ogryczak</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/packaging-and-releasing-private-python-code-pt-2/" title="Packaging and Releasing Private Python Code (Pt.2)">Packaging and Releasing Private Python Code (Pt.2)</a></li><li><a href="https://www.eventbrite.com/engineering/packaging-and-releasing-private-python-code-pt-1/" title="Packaging and Releasing Private Python Code (Pt.1)">Packaging and Releasing Private Python Code (Pt.1)</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/beck/"><strong>Beck Cronin-Dixon</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/eventbrite-and-seo-how-does-google-find-our-pages/" title="Eventbrite and SEO: How does Google find our pages?">Eventbrite and SEO: How does Google find our pages?</a></li><li><a href="https://www.eventbrite.com/engineering/appeasing-the-search-engine-gods-seo-through-a-coding-perspective/" title="Eventbrite and SEO: The Basics">Eventbrite and SEO: The Basics</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/ben-ilegbodu/"><strong>Ben Ilegbodu</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/why-would-webpack-stop-re-compiling-the-quest-for-micro-apps/" title="Why Would Webpack Stop Re-compiling? (The Quest for Micro-Apps)">Why Would Webpack Stop Re-compiling? (The Quest for Micro-Apps)</a></li><li><a href="https://www.eventbrite.com/engineering/quest-react-micro-apps-single-app-mode/" title="The Quest for React Micro-Apps: Single App Mode">The Quest for React Micro-Apps: Single App Mode</a></li><li><a href="https://www.eventbrite.com/engineering/quest-react-micro-apps-beginning/" title="The Quest for React Micro-Apps: The Beginning">The Quest for React Micro-Apps: The Beginning</a></li><li><a href="https://www.eventbrite.com/engineering/learning-es6-generators-as-iterators/" title="Learning ES6: Generators as Iterators">Learning ES6: Generators as Iterators</a></li><li><a href="https://www.eventbrite.com/engineering/learning-es6-iterators-iterables/" title="Learning ES6: Iterators &#038; iterables">Learning ES6: Iterators &#038; iterables</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/bryan/"><strong>Bryan Mayes</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/software-developers-to-nashville-stop-calling-us-it/" title="Software Developers to Nashville, &#8220;Stop calling us IT&#8221;">Software Developers to Nashville, &#8220;Stop calling us IT&#8221;</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/colink/"><strong>Colin Klein</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/as-eventbrite-engineering-leans-into-team-owned-infrastructure-or-devops-were-learning-a-lot-of-new-technologies-in-order-to-stand-up-our-infrastructure/" title="Monitoring Your System">Monitoring Your System</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/cristian-moyano/"><strong>Cristian Moyano</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/descubre-como-se-vive-una-hackaton-en-eventbrite/" title="Descubre cómo se vive una hackatón en Eventbrite">Descubre cómo se vive una hackatón en Eventbrite</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/dcarter/"><strong>Daniel Carter</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/create-reusable-react-file-uploaders/" title="Creating Flexible and Reusable React File Uploaders">Creating Flexible and Reusable React File Uploaders</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/delaine/"><strong>Delaine Wendling</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/britebytes-maddie-cousens/" title="BriteBytes: Maddie Cousens">BriteBytes: Maddie Cousens</a></li><li><a href="https://www.eventbrite.com/engineering/britebytes-nam-chi-van/" title="BriteBytes: Nam-Chi Van">BriteBytes: Nam-Chi Van</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/dhivya/"><strong>Dhivya Sabapathi</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/combatting-csrf-at-eventbrite/" title="Combatting CSRF at Eventbrite: Safeguarding Strategy">Combatting CSRF at Eventbrite: Safeguarding Strategy</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/dmicol/"><strong>Daniel Micol</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/a-day-in-the-life-of-a-technical-fellow/" title="A day in the life of a Technical Fellow">A day in the life of a Technical Fellow</a></li><li><a href="https://www.eventbrite.com/engineering/writing-our-golden-path/" title="Writing our Golden Path">Writing our Golden Path</a></li><li><a href="https://www.eventbrite.com/engineering/writing-our-3-year-technical-vision/" title="Writing our 3-year technical vision">Writing our 3-year technical vision</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/ed/"><strong>Ed Presz</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/mysql-high-availability-at-eventbrite/" title="MySQL High Availability at Eventbrite">MySQL High Availability at Eventbrite</a></li><li><a href="https://www.eventbrite.com/engineering/building-a-protest-map-a-behind-the-scenes-look/" title="Building a Protest Map: A Behind the Scenes Look!">Building a Protest Map: A Behind the Scenes Look!</a></li><li><a href="https://www.eventbrite.com/engineering/teaching-new-presto-performance-tricks-to-the-old-school-dba/" title="Teaching new Presto performance tricks to the Old-School DBA">Teaching new Presto performance tricks to the Old-School DBA</a></li><li><a href="https://www.eventbrite.com/engineering/leveraging-spot-instances-to-drive-down-costs/" title="Leveraging AWS &#8220;spot&#8221; instances to drive down costs">Leveraging AWS &#8220;spot&#8221; instances to drive down costs</a></li><li><a href="https://www.eventbrite.com/engineering/big-data-workloads-presto-auto-scaling/" title="Boosting Big Data workloads with Presto Auto Scaling">Boosting Big Data workloads with Presto Auto Scaling</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/elizabeth-and-loretta/"><strong>Elizabeth Viera &amp; Loretta Stokes</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/what-top-minds-communicated-at-hopperx1-conference-seattle/" title="What the Top Minds in Tech Communicated at Hopperx1 Seattle">What the Top Minds in Tech Communicated at Hopperx1 Seattle</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/eventbrite/"><strong>eventbrite</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/sapna-nair-eventbrite-vp-of-engineering-india/" title="3 Questions With Sapna Nair — Eventbrite’s New VP of Engineering in India">3 Questions With Sapna Nair — Eventbrite’s New VP of Engineering in India</a></li><li><a href="https://www.eventbrite.com/engineering/isomorphic-react-sans-node/" title="Isomorphic React Sans Node">Isomorphic React Sans Node</a></li><li><a href="https://www.eventbrite.com/engineering/react-es-next-%e2%9d%a4/" title="React + ES.next = ❤">React + ES.next = ❤</a></li><li><a href="https://www.eventbrite.com/engineering/3532-2/" title="Engineering + Accounting for Marketplace Businesses">Engineering + Accounting for Marketplace Businesses</a></li><li><a href="https://www.eventbrite.com/engineering/escapandome-de-las-software-factory/" title="Escapándome de las Software Factory">Escapándome de las Software Factory</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/eyal/"><strong>Eyal Reuveni</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/replayable-pubsub-queues-with-cassandra-and-zookeeper/" title="Replayable Pub/Sub Queues with Cassandra and ZooKeeper">Replayable Pub/Sub Queues with Cassandra and ZooKeeper</a></li><li><a href="https://www.eventbrite.com/engineering/smarter-unit-testing-with-nose-knows/" title="Smarter Unit Testing with nose-knows">Smarter Unit Testing with nose-knows</a></li><li><a href="https://www.eventbrite.com/engineering/watching-metadata-changes-in-a-distributed-application-using-zookeeper/" title="Watching Metadata Changes in a Distributed Application Using ZooKeeper">Watching Metadata Changes in a Distributed Application Using ZooKeeper</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/gago/"><strong>Gago</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/the-realistic-code-reviewer-part-ii/" title="The Realistic Code Reviewer, Part II">The Realistic Code Reviewer, Part II</a></li><li><a href="https://www.eventbrite.com/engineering/the-realistic-code-reviewer-part-i/" title="The Realistic Code Reviewer, Part I">The Realistic Code Reviewer, Part I</a></li><li><a href="https://www.eventbrite.com/engineering/code-review-the-art-of-writing-code-for-others/" title="Code Review: The art of writing code for others">Code Review: The art of writing code for others</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/giro/"><strong>Diego Girotti</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/bugs-de-la-vida-cotidiana/" title="Bugs de la vida cotidiana">Bugs de la vida cotidiana</a></li><li><a href="https://www.eventbrite.com/engineering/8-reasons-why-manual-testing-is-still-important/" title="8 Reasons Why Manual Testing is Still Important">8 Reasons Why Manual Testing is Still Important</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/hanah/"><strong>Hanah</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/create-meaningful-and-fun-remote-community/" title="Create Meaningful (and Fun!) Remote Community">Create Meaningful (and Fun!) Remote Community</a></li><li><a href="https://www.eventbrite.com/engineering/how-to-be-a-successful-junior-engineer/" title="How to be a Successful Junior Engineer">How to be a Successful Junior Engineer</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/james-reichardt/"><strong>james reichardt</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/reflecting-on-the-eventbrite-journey-from-centralized-ops-to-devops/" title="Reflecting on Eventbrite’s Journey From Centralized Ops to DevOps">Reflecting on Eventbrite’s Journey From Centralized Ops to DevOps</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/jay/"><strong>Jay Chan</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/multi-index-locality-sensitive-hashing-for-fun-and-profit/" title="Multi-Index Locality Sensitive Hashing for Fun and Profit">Multi-Index Locality Sensitive Hashing for Fun and Profit</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/jcfant/"><strong>JC Fant IV</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/creating-the-3-year-frontend-strategy/" title="Creating the 3 Year Frontend Strategy">Creating the 3 Year Frontend Strategy</a></li><li><a href="https://www.eventbrite.com/engineering/creating-a-3-year-frontend-vision/" title="Creating a 3 Year Frontend Vision">Creating a 3 Year Frontend Vision</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/jessicakatz/"><strong>Jessica Katz</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/the-truth-about-boundaries-curiosity-and-requests-part-2-of-2/" title="The Truth about Boundaries, Curiosity, and Requests (Part 2 of 2)">The Truth about Boundaries, Curiosity, and Requests (Part 2 of 2)</a></li><li><a href="https://www.eventbrite.com/engineering/the-truth-about-boundaries-curiosity-and-requests-part-1-of-2/" title="The Truth about Boundaries, Curiosity, and Requests (Part 1 of 2)">The Truth about Boundaries, Curiosity, and Requests (Part 1 of 2)</a></li><li><a href="https://www.eventbrite.com/engineering/the-lies-we-tell-ourselves/" title="The Lies We Tell Ourselves">The Lies We Tell Ourselves</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/jiangyue/"><strong>Jiangyue Zhu</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/a-story-of-a-react-re-rendering-bug/" title="A Story of a React Re-Rendering Bug">A Story of a React Re-Rendering Bug</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/john-berryman/"><strong>John Berryman</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/fundamental-problem-search/" title="The Fundamental Problem of Search">The Fundamental Problem of Search</a></li><li><a href="https://www.eventbrite.com/engineering/cowboys-and-consultants-dont-need-unit-tests/" title="Cowboys and Consultants Don&#8217;t Need Unit Tests">Cowboys and Consultants Don&#8217;t Need Unit Tests</a></li><li><a href="https://www.eventbrite.com/engineering/search-precision-and-recall-by-example/" title="Search Precision and Recall By Example">Search Precision and Recall By Example</a></li><li><a href="https://www.eventbrite.com/engineering/building-a-marketplace-search-and-recommendation-at-eventbrite/" title="Building a Marketplace &#8212; Search and Recommendation at Eventbrite">Building a Marketplace &#8212; Search and Recommendation at Eventbrite</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/joni/"><strong>Jonathan Rodriguez</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/poder-del-aprendizaje-temprano/" title="El poder del aprendizaje temprano">El poder del aprendizaje temprano</a></li><li><a href="https://www.eventbrite.com/engineering/una-actividad-de-colaboracion-inter-equipos/" title="Una actividad de colaboración inter-equipos">Una actividad de colaboración inter-equipos</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/karishma/"><strong>Karishma Agrawal</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/mvi-model-view-intent-architecture-in-android/" title="MVI [Model View Intent] architecture in Android">MVI [Model View Intent] architecture in Android</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/kyleludvik/"><strong>Kyle Ludvik</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/product-development-process/" title="The Power of Collaboration in Product Development">The Power of Collaboration in Product Development</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/loretta/"><strong>Loretta Stokes</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/grace-hopper-2018-five-unforgettable-experiences/" title="Grace Hopper 2018: Five Unforgettable Experiences">Grace Hopper 2018: Five Unforgettable Experiences</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/luru/"><strong>Luis Ruiz Pavon</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/packaging-generated-code-from-protobuf-files-for-grpc-services/" title="Packaging generated code from protobuf files for gRPC Services">Packaging generated code from protobuf files for gRPC Services</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/malina/"><strong>Malina Wiesen</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/mother-may-i/" title="Mother May I?">Mother May I?</a></li><li><a href="https://www.eventbrite.com/engineering/the-lifecycle-of-an-eventbrite-webhook/" title="The Lifecycle of an Eventbrite Webhook">The Lifecycle of an Eventbrite Webhook</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/marcos/"><strong>Marcos Iglesias</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/discover-pro-d3-js-new-book-improve-your-javascript-data-visualizations/" title="Discover &#8220;Pro D3.js&#8221;, a new book to improve your JavaScript data visualizations">Discover &#8220;Pro D3.js&#8221;, a new book to improve your JavaScript data visualizations</a></li><li><a href="https://www.eventbrite.com/engineering/simple-easy-mentorship-mentoring-agreement/" title="Simple and Easy Mentorship with a Mentoring Agreement">Simple and Easy Mentorship with a Mentoring Agreement</a></li><li><a href="https://www.eventbrite.com/engineering/britecharts-v2-0-released/" title="Britecharts v2.0 Released">Britecharts v2.0 Released</a></li><li><a href="https://www.eventbrite.com/engineering/introducing-britecharts/" title="Introducing Britecharts: Eventbrite&#8217;s Reusable Charting Library Based on D3">Introducing Britecharts: Eventbrite&#8217;s Reusable Charting Library Based on D3</a></li><li><a href="https://www.eventbrite.com/engineering/leveling-up-d3-events-and-refactorings/" title="Leveling Up D3: Events and Refactorings">Leveling Up D3: Events and Refactorings</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/martinb/"><strong>Martin Brambati</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/how-are-you-building-or-maintaining-team-cohesion/" title="How are you building/maintaining team cohesion?">How are you building/maintaining team cohesion?</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/matthew-himelstein/"><strong>Matthew Himelstein</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/the-63-point-plan-for-helping-your-remote-team-succeed/" title="The 63-point Plan for Helping Your Remote Team Succeed">The 63-point Plan for Helping Your Remote Team Succeed</a></li><li><a href="https://www.eventbrite.com/engineering/make-next-event-app-remarkable-4-mobile-navigation-gestures/" title="How to Make Your Next Event App Remarkable with these 4 Mobile Navigation Gestures">How to Make Your Next Event App Remarkable with these 4 Mobile Navigation Gestures</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/meguiluz/"><strong>Maria Eguiluz</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/design-system-wednesday-a-supportive-professional-community/" title="Design System Wednesday: A Supportive Professional Community">Design System Wednesday: A Supportive Professional Community</a></li><li><a href="https://www.eventbrite.com/engineering/how-to-make-swift-product-changes-using-a-design-system/" title="How to Make Swift Product Changes Using a Design System">How to Make Swift Product Changes Using a Design System</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/melisa/"><strong>Melisa Piccinetti</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/be-the-change/" title="Be the change">Be the change</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/miguel-hernandez/"><strong>Miguel Hernandez</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/eventbrite-engineering-pycones/" title="Eventbrite Engineering at PyConES">Eventbrite Engineering at PyConES</a></li><li><a href="https://www.eventbrite.com/engineering/getting-started-unit-tests/" title="Getting started with Unit Tests">Getting started with Unit Tests</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/nati/"><strong>Natalia Cortese</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/open-data-what-why-and-how/" title="Open Data: The what, why and how to get started">Open Data: The what, why and how to get started</a></li><li><a href="https://www.eventbrite.com/engineering/ser-el-cambio/" title="Ser el cambio">Ser el cambio</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/pat/"><strong>Pat Poels</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/the-aha-moments-of-becoming-an-engineering-manager/" title="The “Aha” Moments of Becoming an Engineering Manager">The “Aha” Moments of Becoming an Engineering Manager</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/randall/"><strong>Randall Kanna</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/how-to-support-junior-engineers/" title="How Your Company Can Support Junior Engineers">How Your Company Can Support Junior Engineers</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/rashad/"><strong>Rashad Russell</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/6-unsuspecting-problems-in-html-css-and-javascript-part-2/" title="6 Unsuspecting Problems in HTML, CSS, and JavaScript &#8211; Part 2">6 Unsuspecting Problems in HTML, CSS, and JavaScript &#8211; Part 2</a></li><li><a href="https://www.eventbrite.com/engineering/6-unsuspecting-problems-in-html-css-and-javascript-part-1/" title="6 Unsuspecting Problems in HTML, CSS, and JavaScript &#8211; Part 1">6 Unsuspecting Problems in HTML, CSS, and JavaScript &#8211; Part 1</a></li><li><a href="https://www.eventbrite.com/engineering/6-unsuspecting-problems-in-html-css-and-javascript-part-3/" title="6 Unsuspecting Problems in HTML, CSS, and JavaScript &#8211; Part 3">6 Unsuspecting Problems in HTML, CSS, and JavaScript &#8211; Part 3</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/santiago/"><strong>Santiago Hollmann</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/5-good-practices-when-using-git/" title="5 Good Practices I Follow When I Code Using Git">5 Good Practices I Follow When I Code Using Git</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/senna/"><strong>Sahar Bala</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/how-to-fix-the-ugly-focus-ring-and-not-break-accessibility-in-react/" title="How to fix the ugly focus ring and not break accessibility in React">How to fix the ugly focus ring and not break accessibility in React</a></li><li><a href="https://www.eventbrite.com/engineering/the-best-way-to-hire-qa-engineers/" title="What is the best way to hire QA Engineers?">What is the best way to hire QA Engineers?</a></li><li><a href="https://www.eventbrite.com/engineering/how-to-move-from-customer-support-to-engineering-in-5-steps/" title="How To Move From Customer Support to Engineering in 5 Steps">How To Move From Customer Support to Engineering in 5 Steps</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/stephanie-pi/"><strong>Stephanie Pi</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/getting-the-most-out-of-react-alicante/" title="Getting the most out of React Alicante">Getting the most out of React Alicante</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/steve/"><strong>Steve French</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/heavy-hitters-in-redis/" title="Heavy Hitters in Redis">Heavy Hitters in Redis</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/tchu/"><strong>Tamara Chu</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/britebytes-diego-kartones-munoz/" title="BriteBytes: Diego &#8220;Kartones&#8221; Muñoz">BriteBytes: Diego &#8220;Kartones&#8221; Muñoz</a></li><li><a href="https://www.eventbrite.com/engineering/styleguide-driven-development-at-eventbrite-introduction/" title="Styleguide-Driven Development at Eventbrite: Introduction">Styleguide-Driven Development at Eventbrite: Introduction</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/tominsam/"><strong>Tom Insam</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/setting-the-title-of-airdrop-shares-under-ios-7/" title="Setting the title of AirDrop shares under iOS 7">Setting the title of AirDrop shares under iOS 7</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/toph/"><strong>Toph Burns</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/how-to-craft-a-successful-engineering-interview/" title="How to Craft a Successful Engineering Interview">How to Craft a Successful Engineering Interview</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/victoria-zhang/"><strong>Victoria Zhang</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/the-elevator-pitch-from-a-data-strategist/" title="The Elevator Pitch from a Data Strategist">The Elevator Pitch from a Data Strategist</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/vincent/"><strong>Vincent Budrovich</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/varnish-b-testing-play-nice/" title="Varnish and A-B Testing: How to Play Nice">Varnish and A-B Testing: How to Play Nice</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/vivek/"><strong>Vivek Sagi</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/how-we-created-our-3-year-technical-vision/" title="How we created our 3-year technical vision">How we created our 3-year technical vision</a></li></ul></li><li><a href="https://www.eventbrite.com/engineering/author/zelal/"><strong>Zelal Gungordu</strong></a><ul><li><a href="https://www.eventbrite.com/engineering/automating-relevance-tuning-for-event-search/" title="Automating Relevance Tuning for Event Search">Automating Relevance Tuning for Event Search</a></li></ul></li></ul></section></aside><!-- #secondary --> </div><!-- .wrap --> </div><!-- #content --> <footer id="colophon" class="site-footer" role="contentinfo"> <div class="wrap"> <div class="site-info"> <a href="https://wordpress.org/">Proudly powered by WordPress</a> </div><!-- .site-info --> </div><!-- .wrap --> </footer><!-- #colophon --> </div><!-- .site-content-contain --> </div><!-- #page --> <style id='core-block-supports-inline-css' type='text/css'> .wp-elements-60f749d52976e1bcf39943451888a111 a:where(:not(.wp-element-button)){color:var(--wp--preset--color--black);}.wp-elements-b9f34c3a210a8fdfa3514552f0e02ba8 a:where(:not(.wp-element-button)){color:var(--wp--preset--color--black);}.wp-elements-a8c95a7025c930fe5f226ec82c1d6c13 a:where(:not(.wp-element-button)){color:var(--wp--preset--color--black);} </style> <script type="text/javascript" id="ppress-frontend-script-js-extra"> /* <![CDATA[ */ var pp_ajax_form = {"ajaxurl":"https:\/\/www.eventbrite.com\/engineering\/wp-admin\/admin-ajax.php","confirm_delete":"Are you sure?","deleting_text":"Deleting...","deleting_error":"An error occurred. Please try again.","nonce":"4281e991c7","disable_ajax_form":"false","is_checkout":"0","is_checkout_tax_enabled":"0","is_checkout_autoscroll_enabled":"true"}; /* ]]> */ </script> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-content/plugins/wp-user-avatar/assets/js/frontend.min.js?ver=4.15.17" id="ppress-frontend-script-js"></script> <script type="text/javascript" id="twentyseventeen-skip-link-focus-fix-js-extra"> /* <![CDATA[ */ var twentyseventeenScreenReaderText = {"quote":"<svg class=\"icon icon-quote-right\" aria-hidden=\"true\" role=\"img\"> <use href=\"#icon-quote-right\" xlink:href=\"#icon-quote-right\"><\/use> <\/svg>","expand":"Expand child menu","collapse":"Collapse child menu","icon":"<svg class=\"icon icon-angle-down\" aria-hidden=\"true\" role=\"img\"> <use href=\"#icon-angle-down\" xlink:href=\"#icon-angle-down\"><\/use> <span class=\"svg-fallback icon-angle-down\"><\/span><\/svg>"}; /* ]]> */ </script> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-content/themes/eng_blog_27/assets/js/skip-link-focus-fix.js?ver=1.0" id="twentyseventeen-skip-link-focus-fix-js"></script> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-content/themes/eng_blog_27/assets/js/navigation.js?ver=1.0" id="twentyseventeen-navigation-js"></script> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-content/themes/eng_blog_27/assets/js/global.js?ver=1.0" id="twentyseventeen-global-js"></script> <script type="text/javascript" src="https://www.eventbrite.com/engineering/wp-content/themes/eng_blog_27/assets/js/jquery.scrollTo.js?ver=2.1.2" id="jquery-scrollto-js"></script> <script type="text/javascript" src="https://stats.wp.com/e-202448.js" id="jetpack-stats-js" data-wp-strategy="defer"></script> <script type="text/javascript" id="jetpack-stats-js-after"> /* <![CDATA[ */ _stq = window._stq || []; _stq.push([ "view", JSON.parse("{\"v\":\"ext\",\"blog\":\"116845345\",\"post\":\"0\",\"tz\":\"-8\",\"srv\":\"www.eventbrite.com\",\"j\":\"1:13.9.1\"}") ]); _stq.push([ "clickTrackerInit", "116845345", "0" ]); /* ]]> */ </script> <svg style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <symbol id="icon-behance" viewBox="0 0 37 32"> <path class="path1" d="M33 6.054h-9.125v2.214h9.125v-2.214zM28.5 13.661q-1.607 0-2.607 0.938t-1.107 2.545h7.286q-0.321-3.482-3.571-3.482zM28.786 24.107q1.125 0 2.179-0.571t1.357-1.554h3.946q-1.786 5.482-7.625 5.482-3.821 0-6.080-2.357t-2.259-6.196q0-3.714 2.33-6.17t6.009-2.455q2.464 0 4.295 1.214t2.732 3.196 0.902 4.429q0 0.304-0.036 0.839h-11.75q0 1.982 1.027 3.063t2.973 1.080zM4.946 23.214h5.286q3.661 0 3.661-2.982 0-3.214-3.554-3.214h-5.393v6.196zM4.946 13.625h5.018q1.393 0 2.205-0.652t0.813-2.027q0-2.571-3.393-2.571h-4.643v5.25zM0 4.536h10.607q1.554 0 2.768 0.25t2.259 0.848 1.607 1.723 0.563 2.75q0 3.232-3.071 4.696 2.036 0.571 3.071 2.054t1.036 3.643q0 1.339-0.438 2.438t-1.179 1.848-1.759 1.268-2.161 0.75-2.393 0.232h-10.911v-22.5z"></path> </symbol> <symbol id="icon-deviantart" viewBox="0 0 18 32"> <path class="path1" d="M18.286 5.411l-5.411 10.393 0.429 0.554h4.982v7.411h-9.054l-0.786 0.536-2.536 4.875-0.536 0.536h-5.375v-5.411l5.411-10.411-0.429-0.536h-4.982v-7.411h9.054l0.786-0.536 2.536-4.875 0.536-0.536h5.375v5.411z"></path> </symbol> <symbol id="icon-medium" viewBox="0 0 32 32"> <path class="path1" d="M10.661 7.518v20.946q0 0.446-0.223 0.759t-0.652 0.313q-0.304 0-0.589-0.143l-8.304-4.161q-0.375-0.179-0.634-0.598t-0.259-0.83v-20.357q0-0.357 0.179-0.607t0.518-0.25q0.25 0 0.786 0.268l9.125 4.571q0.054 0.054 0.054 0.089zM11.804 9.321l9.536 15.464-9.536-4.75v-10.714zM32 9.643v18.821q0 0.446-0.25 0.723t-0.679 0.277-0.839-0.232l-7.875-3.929zM31.946 7.5q0 0.054-4.58 7.491t-5.366 8.705l-6.964-11.321 5.786-9.411q0.304-0.5 0.929-0.5 0.25 0 0.464 0.107l9.661 4.821q0.071 0.036 0.071 0.107z"></path> </symbol> <symbol id="icon-slideshare" viewBox="0 0 32 32"> <path class="path1" d="M15.589 13.214q0 1.482-1.134 2.545t-2.723 1.063-2.723-1.063-1.134-2.545q0-1.5 1.134-2.554t2.723-1.054 2.723 1.054 1.134 2.554zM24.554 13.214q0 1.482-1.125 2.545t-2.732 1.063q-1.589 0-2.723-1.063t-1.134-2.545q0-1.5 1.134-2.554t2.723-1.054q1.607 0 2.732 1.054t1.125 2.554zM28.571 16.429v-11.911q0-1.554-0.571-2.205t-1.982-0.652h-19.857q-1.482 0-2.009 0.607t-0.527 2.25v12.018q0.768 0.411 1.58 0.714t1.446 0.5 1.446 0.33 1.268 0.196 1.25 0.071 1.045 0.009 1.009-0.036 0.795-0.036q1.214-0.018 1.696 0.482 0.107 0.107 0.179 0.161 0.464 0.446 1.089 0.911 0.125-1.625 2.107-1.554 0.089 0 0.652 0.027t0.768 0.036 0.813 0.018 0.946-0.018 0.973-0.080 1.089-0.152 1.107-0.241 1.196-0.348 1.205-0.482 1.286-0.616zM31.482 16.339q-2.161 2.661-6.643 4.5 1.5 5.089-0.411 8.304-1.179 2.018-3.268 2.643-1.857 0.571-3.25-0.268-1.536-0.911-1.464-2.929l-0.018-5.821v-0.018q-0.143-0.036-0.438-0.107t-0.42-0.089l-0.018 6.036q0.071 2.036-1.482 2.929-1.411 0.839-3.268 0.268-2.089-0.643-3.25-2.679-1.875-3.214-0.393-8.268-4.482-1.839-6.643-4.5-0.446-0.661-0.071-1.125t1.071 0.018q0.054 0.036 0.196 0.125t0.196 0.143v-12.393q0-1.286 0.839-2.196t2.036-0.911h22.446q1.196 0 2.036 0.911t0.839 2.196v12.393l0.375-0.268q0.696-0.482 1.071-0.018t-0.071 1.125z"></path> </symbol> <symbol id="icon-snapchat-ghost" viewBox="0 0 30 32"> <path class="path1" d="M15.143 2.286q2.393-0.018 4.295 1.223t2.92 3.438q0.482 1.036 0.482 3.196 0 0.839-0.161 3.411 0.25 0.125 0.5 0.125 0.321 0 0.911-0.241t0.911-0.241q0.518 0 1 0.321t0.482 0.821q0 0.571-0.563 0.964t-1.232 0.563-1.232 0.518-0.563 0.848q0 0.268 0.214 0.768 0.661 1.464 1.83 2.679t2.58 1.804q0.5 0.214 1.429 0.411 0.5 0.107 0.5 0.625 0 1.25-3.911 1.839-0.125 0.196-0.196 0.696t-0.25 0.83-0.589 0.33q-0.357 0-1.107-0.116t-1.143-0.116q-0.661 0-1.107 0.089-0.571 0.089-1.125 0.402t-1.036 0.679-1.036 0.723-1.357 0.598-1.768 0.241q-0.929 0-1.723-0.241t-1.339-0.598-1.027-0.723-1.036-0.679-1.107-0.402q-0.464-0.089-1.125-0.089-0.429 0-1.17 0.134t-1.045 0.134q-0.446 0-0.625-0.33t-0.25-0.848-0.196-0.714q-3.911-0.589-3.911-1.839 0-0.518 0.5-0.625 0.929-0.196 1.429-0.411 1.393-0.571 2.58-1.804t1.83-2.679q0.214-0.5 0.214-0.768 0-0.5-0.563-0.848t-1.241-0.527-1.241-0.563-0.563-0.938q0-0.482 0.464-0.813t0.982-0.33q0.268 0 0.857 0.232t0.946 0.232q0.321 0 0.571-0.125-0.161-2.536-0.161-3.393 0-2.179 0.482-3.214 1.143-2.446 3.071-3.536t4.714-1.125z"></path> </symbol> <symbol id="icon-yelp" viewBox="0 0 27 32"> <path class="path1" d="M13.804 23.554v2.268q-0.018 5.214-0.107 5.446-0.214 0.571-0.911 0.714-0.964 0.161-3.241-0.679t-2.902-1.589q-0.232-0.268-0.304-0.643-0.018-0.214 0.071-0.464 0.071-0.179 0.607-0.839t3.232-3.857q0.018 0 1.071-1.25 0.268-0.339 0.705-0.438t0.884 0.063q0.429 0.179 0.67 0.518t0.223 0.75zM11.143 19.071q-0.054 0.982-0.929 1.25l-2.143 0.696q-4.911 1.571-5.214 1.571-0.625-0.036-0.964-0.643-0.214-0.446-0.304-1.339-0.143-1.357 0.018-2.973t0.536-2.223 1-0.571q0.232 0 3.607 1.375 1.25 0.518 2.054 0.839l1.5 0.607q0.411 0.161 0.634 0.545t0.205 0.866zM25.893 24.375q-0.125 0.964-1.634 2.875t-2.42 2.268q-0.661 0.25-1.125-0.125-0.25-0.179-3.286-5.125l-0.839-1.375q-0.25-0.375-0.205-0.821t0.348-0.821q0.625-0.768 1.482-0.464 0.018 0.018 2.125 0.714 3.625 1.179 4.321 1.42t0.839 0.366q0.5 0.393 0.393 1.089zM13.893 13.089q0.089 1.821-0.964 2.179-1.036 0.304-2.036-1.268l-6.75-10.679q-0.143-0.625 0.339-1.107 0.732-0.768 3.705-1.598t4.009-0.563q0.714 0.179 0.875 0.804 0.054 0.321 0.393 5.455t0.429 6.777zM25.714 15.018q0.054 0.696-0.464 1.054-0.268 0.179-5.875 1.536-1.196 0.268-1.625 0.411l0.018-0.036q-0.411 0.107-0.821-0.071t-0.661-0.571q-0.536-0.839 0-1.554 0.018-0.018 1.339-1.821 2.232-3.054 2.679-3.643t0.607-0.696q0.5-0.339 1.161-0.036 0.857 0.411 2.196 2.384t1.446 2.991v0.054z"></path> </symbol> <symbol id="icon-vine" viewBox="0 0 27 32"> <path class="path1" d="M26.732 14.768v3.536q-1.804 0.411-3.536 0.411-1.161 2.429-2.955 4.839t-3.241 3.848-2.286 1.902q-1.429 0.804-2.893-0.054-0.5-0.304-1.080-0.777t-1.518-1.491-1.83-2.295-1.92-3.286-1.884-4.357-1.634-5.616-1.259-6.964h5.054q0.464 3.893 1.25 7.116t1.866 5.661 2.17 4.205 2.5 3.482q3.018-3.018 5.125-7.25-2.536-1.286-3.982-3.929t-1.446-5.946q0-3.429 1.857-5.616t5.071-2.188q3.179 0 4.875 1.884t1.696 5.313q0 2.839-1.036 5.107-0.125 0.018-0.348 0.054t-0.821 0.036-1.125-0.107-1.107-0.455-0.902-0.92q0.554-1.839 0.554-3.286 0-1.554-0.518-2.357t-1.411-0.804q-0.946 0-1.518 0.884t-0.571 2.509q0 3.321 1.875 5.241t4.768 1.92q1.107 0 2.161-0.25z"></path> </symbol> <symbol id="icon-vk" viewBox="0 0 35 32"> <path class="path1" d="M34.232 9.286q0.411 1.143-2.679 5.25-0.429 0.571-1.161 1.518-1.393 1.786-1.607 2.339-0.304 0.732 0.25 1.446 0.304 0.375 1.446 1.464h0.018l0.071 0.071q2.518 2.339 3.411 3.946 0.054 0.089 0.116 0.223t0.125 0.473-0.009 0.607-0.446 0.491-1.054 0.223l-4.571 0.071q-0.429 0.089-1-0.089t-0.929-0.393l-0.357-0.214q-0.536-0.375-1.25-1.143t-1.223-1.384-1.089-1.036-1.009-0.277q-0.054 0.018-0.143 0.063t-0.304 0.259-0.384 0.527-0.304 0.929-0.116 1.384q0 0.268-0.063 0.491t-0.134 0.33l-0.071 0.089q-0.321 0.339-0.946 0.393h-2.054q-1.268 0.071-2.607-0.295t-2.348-0.946-1.839-1.179-1.259-1.027l-0.446-0.429q-0.179-0.179-0.491-0.536t-1.277-1.625-1.893-2.696-2.188-3.768-2.33-4.857q-0.107-0.286-0.107-0.482t0.054-0.286l0.071-0.107q0.268-0.339 1.018-0.339l4.893-0.036q0.214 0.036 0.411 0.116t0.286 0.152l0.089 0.054q0.286 0.196 0.429 0.571 0.357 0.893 0.821 1.848t0.732 1.455l0.286 0.518q0.518 1.071 1 1.857t0.866 1.223 0.741 0.688 0.607 0.25 0.482-0.089q0.036-0.018 0.089-0.089t0.214-0.393 0.241-0.839 0.17-1.446 0-2.232q-0.036-0.714-0.161-1.304t-0.25-0.821l-0.107-0.214q-0.446-0.607-1.518-0.768-0.232-0.036 0.089-0.429 0.304-0.339 0.679-0.536 0.946-0.464 4.268-0.429 1.464 0.018 2.411 0.232 0.357 0.089 0.598 0.241t0.366 0.429 0.188 0.571 0.063 0.813-0.018 0.982-0.045 1.259-0.027 1.473q0 0.196-0.018 0.75t-0.009 0.857 0.063 0.723 0.205 0.696 0.402 0.438q0.143 0.036 0.304 0.071t0.464-0.196 0.679-0.616 0.929-1.196 1.214-1.92q1.071-1.857 1.911-4.018 0.071-0.179 0.179-0.313t0.196-0.188l0.071-0.054 0.089-0.045t0.232-0.054 0.357-0.009l5.143-0.036q0.696-0.089 1.143 0.045t0.554 0.295z"></path> </symbol> <symbol id="icon-search" viewBox="0 0 30 32"> <path class="path1" d="M20.571 14.857q0-3.304-2.348-5.652t-5.652-2.348-5.652 2.348-2.348 5.652 2.348 5.652 5.652 2.348 5.652-2.348 2.348-5.652zM29.714 29.714q0 0.929-0.679 1.607t-1.607 0.679q-0.964 0-1.607-0.679l-6.125-6.107q-3.196 2.214-7.125 2.214-2.554 0-4.884-0.991t-4.018-2.679-2.679-4.018-0.991-4.884 0.991-4.884 2.679-4.018 4.018-2.679 4.884-0.991 4.884 0.991 4.018 2.679 2.679 4.018 0.991 4.884q0 3.929-2.214 7.125l6.125 6.125q0.661 0.661 0.661 1.607z"></path> </symbol> <symbol id="icon-envelope-o" viewBox="0 0 32 32"> <path class="path1" d="M29.714 26.857v-13.714q-0.571 0.643-1.232 1.179-4.786 3.679-7.607 6.036-0.911 0.768-1.482 1.196t-1.545 0.866-1.83 0.438h-0.036q-0.857 0-1.83-0.438t-1.545-0.866-1.482-1.196q-2.821-2.357-7.607-6.036-0.661-0.536-1.232-1.179v13.714q0 0.232 0.17 0.402t0.402 0.17h26.286q0.232 0 0.402-0.17t0.17-0.402zM29.714 8.089v-0.438t-0.009-0.232-0.054-0.223-0.098-0.161-0.161-0.134-0.25-0.045h-26.286q-0.232 0-0.402 0.17t-0.17 0.402q0 3 2.625 5.071 3.446 2.714 7.161 5.661 0.107 0.089 0.625 0.527t0.821 0.67 0.795 0.563 0.902 0.491 0.768 0.161h0.036q0.357 0 0.768-0.161t0.902-0.491 0.795-0.563 0.821-0.67 0.625-0.527q3.714-2.946 7.161-5.661 0.964-0.768 1.795-2.063t0.83-2.348zM32 7.429v19.429q0 1.179-0.839 2.018t-2.018 0.839h-26.286q-1.179 0-2.018-0.839t-0.839-2.018v-19.429q0-1.179 0.839-2.018t2.018-0.839h26.286q1.179 0 2.018 0.839t0.839 2.018z"></path> </symbol> <symbol id="icon-close" viewBox="0 0 25 32"> <path class="path1" d="M23.179 23.607q0 0.714-0.5 1.214l-2.429 2.429q-0.5 0.5-1.214 0.5t-1.214-0.5l-5.25-5.25-5.25 5.25q-0.5 0.5-1.214 0.5t-1.214-0.5l-2.429-2.429q-0.5-0.5-0.5-1.214t0.5-1.214l5.25-5.25-5.25-5.25q-0.5-0.5-0.5-1.214t0.5-1.214l2.429-2.429q0.5-0.5 1.214-0.5t1.214 0.5l5.25 5.25 5.25-5.25q0.5-0.5 1.214-0.5t1.214 0.5l2.429 2.429q0.5 0.5 0.5 1.214t-0.5 1.214l-5.25 5.25 5.25 5.25q0.5 0.5 0.5 1.214z"></path> </symbol> <symbol id="icon-angle-down" viewBox="0 0 21 32"> <path class="path1" d="M19.196 13.143q0 0.232-0.179 0.411l-8.321 8.321q-0.179 0.179-0.411 0.179t-0.411-0.179l-8.321-8.321q-0.179-0.179-0.179-0.411t0.179-0.411l0.893-0.893q0.179-0.179 0.411-0.179t0.411 0.179l7.018 7.018 7.018-7.018q0.179-0.179 0.411-0.179t0.411 0.179l0.893 0.893q0.179 0.179 0.179 0.411z"></path> </symbol> <symbol id="icon-folder-open" viewBox="0 0 34 32"> <path class="path1" d="M33.554 17q0 0.554-0.554 1.179l-6 7.071q-0.768 0.911-2.152 1.545t-2.563 0.634h-19.429q-0.607 0-1.080-0.232t-0.473-0.768q0-0.554 0.554-1.179l6-7.071q0.768-0.911 2.152-1.545t2.563-0.634h19.429q0.607 0 1.080 0.232t0.473 0.768zM27.429 10.857v2.857h-14.857q-1.679 0-3.518 0.848t-2.929 2.134l-6.107 7.179q0-0.071-0.009-0.223t-0.009-0.223v-17.143q0-1.643 1.179-2.821t2.821-1.179h5.714q1.643 0 2.821 1.179t1.179 2.821v0.571h9.714q1.643 0 2.821 1.179t1.179 2.821z"></path> </symbol> <symbol id="icon-twitter" viewBox="0 0 30 32"> <path class="path1" d="M28.929 7.286q-1.196 1.75-2.893 2.982 0.018 0.25 0.018 0.75 0 2.321-0.679 4.634t-2.063 4.437-3.295 3.759-4.607 2.607-5.768 0.973q-4.839 0-8.857-2.589 0.625 0.071 1.393 0.071 4.018 0 7.161-2.464-1.875-0.036-3.357-1.152t-2.036-2.848q0.589 0.089 1.089 0.089 0.768 0 1.518-0.196-2-0.411-3.313-1.991t-1.313-3.67v-0.071q1.214 0.679 2.607 0.732-1.179-0.786-1.875-2.054t-0.696-2.75q0-1.571 0.786-2.911 2.161 2.661 5.259 4.259t6.634 1.777q-0.143-0.679-0.143-1.321 0-2.393 1.688-4.080t4.080-1.688q2.5 0 4.214 1.821 1.946-0.375 3.661-1.393-0.661 2.054-2.536 3.179 1.661-0.179 3.321-0.893z"></path> </symbol> <symbol id="icon-facebook" viewBox="0 0 19 32"> <path class="path1" d="M17.125 0.214v4.714h-2.804q-1.536 0-2.071 0.643t-0.536 1.929v3.375h5.232l-0.696 5.286h-4.536v13.554h-5.464v-13.554h-4.554v-5.286h4.554v-3.893q0-3.321 1.857-5.152t4.946-1.83q2.625 0 4.071 0.214z"></path> </symbol> <symbol id="icon-github" viewBox="0 0 27 32"> <path class="path1" d="M13.714 2.286q3.732 0 6.884 1.839t4.991 4.991 1.839 6.884q0 4.482-2.616 8.063t-6.759 4.955q-0.482 0.089-0.714-0.125t-0.232-0.536q0-0.054 0.009-1.366t0.009-2.402q0-1.732-0.929-2.536 1.018-0.107 1.83-0.321t1.679-0.696 1.446-1.188 0.946-1.875 0.366-2.688q0-2.125-1.411-3.679 0.661-1.625-0.143-3.643-0.5-0.161-1.446 0.196t-1.643 0.786l-0.679 0.429q-1.661-0.464-3.429-0.464t-3.429 0.464q-0.286-0.196-0.759-0.482t-1.491-0.688-1.518-0.241q-0.804 2.018-0.143 3.643-1.411 1.554-1.411 3.679 0 1.518 0.366 2.679t0.938 1.875 1.438 1.196 1.679 0.696 1.83 0.321q-0.696 0.643-0.875 1.839-0.375 0.179-0.804 0.268t-1.018 0.089-1.17-0.384-0.991-1.116q-0.339-0.571-0.866-0.929t-0.884-0.429l-0.357-0.054q-0.375 0-0.518 0.080t-0.089 0.205 0.161 0.25 0.232 0.214l0.125 0.089q0.393 0.179 0.777 0.679t0.563 0.911l0.179 0.411q0.232 0.679 0.786 1.098t1.196 0.536 1.241 0.125 0.991-0.063l0.411-0.071q0 0.679 0.009 1.58t0.009 0.973q0 0.321-0.232 0.536t-0.714 0.125q-4.143-1.375-6.759-4.955t-2.616-8.063q0-3.732 1.839-6.884t4.991-4.991 6.884-1.839zM5.196 21.982q0.054-0.125-0.125-0.214-0.179-0.054-0.232 0.036-0.054 0.125 0.125 0.214 0.161 0.107 0.232-0.036zM5.75 22.589q0.125-0.089-0.036-0.286-0.179-0.161-0.286-0.054-0.125 0.089 0.036 0.286 0.179 0.179 0.286 0.054zM6.286 23.393q0.161-0.125 0-0.339-0.143-0.232-0.304-0.107-0.161 0.089 0 0.321t0.304 0.125zM7.036 24.143q0.143-0.143-0.071-0.339-0.214-0.214-0.357-0.054-0.161 0.143 0.071 0.339 0.214 0.214 0.357 0.054zM8.054 24.589q0.054-0.196-0.232-0.286-0.268-0.071-0.339 0.125t0.232 0.268q0.268 0.107 0.339-0.107zM9.179 24.679q0-0.232-0.304-0.196-0.286 0-0.286 0.196 0 0.232 0.304 0.196 0.286 0 0.286-0.196zM10.214 24.5q-0.036-0.196-0.321-0.161-0.286 0.054-0.25 0.268t0.321 0.143 0.25-0.25z"></path> </symbol> <symbol id="icon-bars" viewBox="0 0 27 32"> <path class="path1" d="M27.429 24v2.286q0 0.464-0.339 0.804t-0.804 0.339h-25.143q-0.464 0-0.804-0.339t-0.339-0.804v-2.286q0-0.464 0.339-0.804t0.804-0.339h25.143q0.464 0 0.804 0.339t0.339 0.804zM27.429 14.857v2.286q0 0.464-0.339 0.804t-0.804 0.339h-25.143q-0.464 0-0.804-0.339t-0.339-0.804v-2.286q0-0.464 0.339-0.804t0.804-0.339h25.143q0.464 0 0.804 0.339t0.339 0.804zM27.429 5.714v2.286q0 0.464-0.339 0.804t-0.804 0.339h-25.143q-0.464 0-0.804-0.339t-0.339-0.804v-2.286q0-0.464 0.339-0.804t0.804-0.339h25.143q0.464 0 0.804 0.339t0.339 0.804z"></path> </symbol> <symbol id="icon-google-plus" viewBox="0 0 41 32"> <path class="path1" d="M25.661 16.304q0 3.714-1.554 6.616t-4.429 4.536-6.589 1.634q-2.661 0-5.089-1.036t-4.179-2.786-2.786-4.179-1.036-5.089 1.036-5.089 2.786-4.179 4.179-2.786 5.089-1.036q5.107 0 8.768 3.429l-3.554 3.411q-2.089-2.018-5.214-2.018-2.196 0-4.063 1.107t-2.955 3.009-1.089 4.152 1.089 4.152 2.955 3.009 4.063 1.107q1.482 0 2.723-0.411t2.045-1.027 1.402-1.402 0.875-1.482 0.384-1.321h-7.429v-4.5h12.357q0.214 1.125 0.214 2.179zM41.143 14.125v3.75h-3.732v3.732h-3.75v-3.732h-3.732v-3.75h3.732v-3.732h3.75v3.732h3.732z"></path> </symbol> <symbol id="icon-linkedin" viewBox="0 0 27 32"> <path class="path1" d="M6.232 11.161v17.696h-5.893v-17.696h5.893zM6.607 5.696q0.018 1.304-0.902 2.179t-2.42 0.875h-0.036q-1.464 0-2.357-0.875t-0.893-2.179q0-1.321 0.92-2.188t2.402-0.866 2.375 0.866 0.911 2.188zM27.429 18.714v10.143h-5.875v-9.464q0-1.875-0.723-2.938t-2.259-1.063q-1.125 0-1.884 0.616t-1.134 1.527q-0.196 0.536-0.196 1.446v9.875h-5.875q0.036-7.125 0.036-11.554t-0.018-5.286l-0.018-0.857h5.875v2.571h-0.036q0.357-0.571 0.732-1t1.009-0.929 1.554-0.777 2.045-0.277q3.054 0 4.911 2.027t1.857 5.938z"></path> </symbol> <symbol id="icon-quote-right" viewBox="0 0 30 32"> <path class="path1" d="M13.714 5.714v12.571q0 1.857-0.723 3.545t-1.955 2.92-2.92 1.955-3.545 0.723h-1.143q-0.464 0-0.804-0.339t-0.339-0.804v-2.286q0-0.464 0.339-0.804t0.804-0.339h1.143q1.893 0 3.232-1.339t1.339-3.232v-0.571q0-0.714-0.5-1.214t-1.214-0.5h-4q-1.429 0-2.429-1t-1-2.429v-6.857q0-1.429 1-2.429t2.429-1h6.857q1.429 0 2.429 1t1 2.429zM29.714 5.714v12.571q0 1.857-0.723 3.545t-1.955 2.92-2.92 1.955-3.545 0.723h-1.143q-0.464 0-0.804-0.339t-0.339-0.804v-2.286q0-0.464 0.339-0.804t0.804-0.339h1.143q1.893 0 3.232-1.339t1.339-3.232v-0.571q0-0.714-0.5-1.214t-1.214-0.5h-4q-1.429 0-2.429-1t-1-2.429v-6.857q0-1.429 1-2.429t2.429-1h6.857q1.429 0 2.429 1t1 2.429z"></path> </symbol> <symbol id="icon-mail-reply" viewBox="0 0 32 32"> <path class="path1" d="M32 20q0 2.964-2.268 8.054-0.054 0.125-0.188 0.429t-0.241 0.536-0.232 0.393q-0.214 0.304-0.5 0.304-0.268 0-0.42-0.179t-0.152-0.446q0-0.161 0.045-0.473t0.045-0.42q0.089-1.214 0.089-2.196 0-1.804-0.313-3.232t-0.866-2.473-1.429-1.804-1.884-1.241-2.375-0.759-2.75-0.384-3.134-0.107h-4v4.571q0 0.464-0.339 0.804t-0.804 0.339-0.804-0.339l-9.143-9.143q-0.339-0.339-0.339-0.804t0.339-0.804l9.143-9.143q0.339-0.339 0.804-0.339t0.804 0.339 0.339 0.804v4.571h4q12.732 0 15.625 7.196 0.946 2.393 0.946 5.946z"></path> </symbol> <symbol id="icon-youtube" viewBox="0 0 27 32"> <path class="path1" d="M17.339 22.214v3.768q0 1.196-0.696 1.196-0.411 0-0.804-0.393v-5.375q0.393-0.393 0.804-0.393 0.696 0 0.696 1.196zM23.375 22.232v0.821h-1.607v-0.821q0-1.214 0.804-1.214t0.804 1.214zM6.125 18.339h1.911v-1.679h-5.571v1.679h1.875v10.161h1.786v-10.161zM11.268 28.5h1.589v-8.821h-1.589v6.75q-0.536 0.75-1.018 0.75-0.321 0-0.375-0.375-0.018-0.054-0.018-0.625v-6.5h-1.589v6.982q0 0.875 0.143 1.304 0.214 0.661 1.036 0.661 0.857 0 1.821-1.089v0.964zM18.929 25.857v-3.518q0-1.304-0.161-1.768-0.304-1-1.268-1-0.893 0-1.661 0.964v-3.875h-1.589v11.839h1.589v-0.857q0.804 0.982 1.661 0.982 0.964 0 1.268-0.982 0.161-0.482 0.161-1.786zM24.964 25.679v-0.232h-1.625q0 0.911-0.036 1.089-0.125 0.643-0.714 0.643-0.821 0-0.821-1.232v-1.554h3.196v-1.839q0-1.411-0.482-2.071-0.696-0.911-1.893-0.911-1.214 0-1.911 0.911-0.5 0.661-0.5 2.071v3.089q0 1.411 0.518 2.071 0.696 0.911 1.929 0.911 1.286 0 1.929-0.946 0.321-0.482 0.375-0.964 0.036-0.161 0.036-1.036zM14.107 9.375v-3.75q0-1.232-0.768-1.232t-0.768 1.232v3.75q0 1.25 0.768 1.25t0.768-1.25zM26.946 22.786q0 4.179-0.464 6.25-0.25 1.054-1.036 1.768t-1.821 0.821q-3.286 0.375-9.911 0.375t-9.911-0.375q-1.036-0.107-1.83-0.821t-1.027-1.768q-0.464-2-0.464-6.25 0-4.179 0.464-6.25 0.25-1.054 1.036-1.768t1.839-0.839q3.268-0.357 9.893-0.357t9.911 0.357q1.036 0.125 1.83 0.839t1.027 1.768q0.464 2 0.464 6.25zM9.125 0h1.821l-2.161 7.125v4.839h-1.786v-4.839q-0.25-1.321-1.089-3.786-0.661-1.839-1.161-3.339h1.893l1.268 4.696zM15.732 5.946v3.125q0 1.446-0.5 2.107-0.661 0.911-1.893 0.911-1.196 0-1.875-0.911-0.5-0.679-0.5-2.107v-3.125q0-1.429 0.5-2.089 0.679-0.911 1.875-0.911 1.232 0 1.893 0.911 0.5 0.661 0.5 2.089zM21.714 3.054v8.911h-1.625v-0.982q-0.946 1.107-1.839 1.107-0.821 0-1.054-0.661-0.143-0.429-0.143-1.339v-7.036h1.625v6.554q0 0.589 0.018 0.625 0.054 0.393 0.375 0.393 0.482 0 1.018-0.768v-6.804h1.625z"></path> </symbol> <symbol id="icon-dropbox" viewBox="0 0 32 32"> <path class="path1" d="M7.179 12.625l8.821 5.446-6.107 5.089-8.75-5.696zM24.786 22.536v1.929l-8.75 5.232v0.018l-0.018-0.018-0.018 0.018v-0.018l-8.732-5.232v-1.929l2.625 1.714 6.107-5.071v-0.036l0.018 0.018 0.018-0.018v0.036l6.125 5.071zM9.893 2.107l6.107 5.089-8.821 5.429-6.036-4.821zM24.821 12.625l6.036 4.839-8.732 5.696-6.125-5.089zM22.125 2.107l8.732 5.696-6.036 4.821-8.821-5.429z"></path> </symbol> <symbol id="icon-instagram" viewBox="0 0 27 32"> <path class="path1" d="M18.286 16q0-1.893-1.339-3.232t-3.232-1.339-3.232 1.339-1.339 3.232 1.339 3.232 3.232 1.339 3.232-1.339 1.339-3.232zM20.75 16q0 2.929-2.054 4.982t-4.982 2.054-4.982-2.054-2.054-4.982 2.054-4.982 4.982-2.054 4.982 2.054 2.054 4.982zM22.679 8.679q0 0.679-0.482 1.161t-1.161 0.482-1.161-0.482-0.482-1.161 0.482-1.161 1.161-0.482 1.161 0.482 0.482 1.161zM13.714 4.75q-0.125 0-1.366-0.009t-1.884 0-1.723 0.054-1.839 0.179-1.277 0.33q-0.893 0.357-1.571 1.036t-1.036 1.571q-0.196 0.518-0.33 1.277t-0.179 1.839-0.054 1.723 0 1.884 0.009 1.366-0.009 1.366 0 1.884 0.054 1.723 0.179 1.839 0.33 1.277q0.357 0.893 1.036 1.571t1.571 1.036q0.518 0.196 1.277 0.33t1.839 0.179 1.723 0.054 1.884 0 1.366-0.009 1.366 0.009 1.884 0 1.723-0.054 1.839-0.179 1.277-0.33q0.893-0.357 1.571-1.036t1.036-1.571q0.196-0.518 0.33-1.277t0.179-1.839 0.054-1.723 0-1.884-0.009-1.366 0.009-1.366 0-1.884-0.054-1.723-0.179-1.839-0.33-1.277q-0.357-0.893-1.036-1.571t-1.571-1.036q-0.518-0.196-1.277-0.33t-1.839-0.179-1.723-0.054-1.884 0-1.366 0.009zM27.429 16q0 4.089-0.089 5.661-0.179 3.714-2.214 5.75t-5.75 2.214q-1.571 0.089-5.661 0.089t-5.661-0.089q-3.714-0.179-5.75-2.214t-2.214-5.75q-0.089-1.571-0.089-5.661t0.089-5.661q0.179-3.714 2.214-5.75t5.75-2.214q1.571-0.089 5.661-0.089t5.661 0.089q3.714 0.179 5.75 2.214t2.214 5.75q0.089 1.571 0.089 5.661z"></path> </symbol> <symbol id="icon-flickr" viewBox="0 0 27 32"> <path class="path1" d="M22.286 2.286q2.125 0 3.634 1.509t1.509 3.634v17.143q0 2.125-1.509 3.634t-3.634 1.509h-17.143q-2.125 0-3.634-1.509t-1.509-3.634v-17.143q0-2.125 1.509-3.634t3.634-1.509h17.143zM12.464 16q0-1.571-1.107-2.679t-2.679-1.107-2.679 1.107-1.107 2.679 1.107 2.679 2.679 1.107 2.679-1.107 1.107-2.679zM22.536 16q0-1.571-1.107-2.679t-2.679-1.107-2.679 1.107-1.107 2.679 1.107 2.679 2.679 1.107 2.679-1.107 1.107-2.679z"></path> </symbol> <symbol id="icon-tumblr" viewBox="0 0 19 32"> <path class="path1" d="M16.857 23.732l1.429 4.232q-0.411 0.625-1.982 1.179t-3.161 0.571q-1.857 0.036-3.402-0.464t-2.545-1.321-1.696-1.893-0.991-2.143-0.295-2.107v-9.714h-3v-3.839q1.286-0.464 2.304-1.241t1.625-1.607 1.036-1.821 0.607-1.768 0.268-1.58q0.018-0.089 0.080-0.152t0.134-0.063h4.357v7.571h5.946v4.5h-5.964v9.25q0 0.536 0.116 1t0.402 0.938 0.884 0.741 1.455 0.25q1.393-0.036 2.393-0.518z"></path> </symbol> <symbol id="icon-dribbble" viewBox="0 0 27 32"> <path class="path1" d="M18.286 26.786q-0.75-4.304-2.5-8.893h-0.036l-0.036 0.018q-0.286 0.107-0.768 0.295t-1.804 0.875-2.446 1.464-2.339 2.045-1.839 2.643l-0.268-0.196q3.286 2.679 7.464 2.679 2.357 0 4.571-0.929zM14.982 15.946q-0.375-0.875-0.946-1.982-5.554 1.661-12.018 1.661-0.018 0.125-0.018 0.375 0 2.214 0.786 4.223t2.214 3.598q0.893-1.589 2.205-2.973t2.545-2.223 2.33-1.446 1.777-0.857l0.661-0.232q0.071-0.018 0.232-0.063t0.232-0.080zM13.071 12.161q-2.143-3.804-4.357-6.75-2.464 1.161-4.179 3.321t-2.286 4.857q5.393 0 10.821-1.429zM25.286 17.857q-3.75-1.071-7.304-0.518 1.554 4.268 2.286 8.375 1.982-1.339 3.304-3.384t1.714-4.473zM10.911 4.625q-0.018 0-0.036 0.018 0.018-0.018 0.036-0.018zM21.446 7.214q-3.304-2.929-7.732-2.929-1.357 0-2.768 0.339 2.339 3.036 4.393 6.821 1.232-0.464 2.321-1.080t1.723-1.098 1.17-1.018 0.67-0.723zM25.429 15.875q-0.054-4.143-2.661-7.321l-0.018 0.018q-0.161 0.214-0.339 0.438t-0.777 0.795-1.268 1.080-1.786 1.161-2.348 1.152q0.446 0.946 0.786 1.696 0.036 0.107 0.116 0.313t0.134 0.295q0.643-0.089 1.33-0.125t1.313-0.036 1.232 0.027 1.143 0.071 1.009 0.098 0.857 0.116 0.652 0.107 0.446 0.080zM27.429 16q0 3.732-1.839 6.884t-4.991 4.991-6.884 1.839-6.884-1.839-4.991-4.991-1.839-6.884 1.839-6.884 4.991-4.991 6.884-1.839 6.884 1.839 4.991 4.991 1.839 6.884z"></path> </symbol> <symbol id="icon-skype" viewBox="0 0 27 32"> <path class="path1" d="M20.946 18.982q0-0.893-0.348-1.634t-0.866-1.223-1.304-0.875-1.473-0.607-1.563-0.411l-1.857-0.429q-0.536-0.125-0.786-0.188t-0.625-0.205-0.536-0.286-0.295-0.375-0.134-0.536q0-1.375 2.571-1.375 0.768 0 1.375 0.214t0.964 0.509 0.679 0.598 0.714 0.518 0.857 0.214q0.839 0 1.348-0.571t0.509-1.375q0-0.982-1-1.777t-2.536-1.205-3.25-0.411q-1.214 0-2.357 0.277t-2.134 0.839-1.589 1.554-0.598 2.295q0 1.089 0.339 1.902t1 1.348 1.429 0.866 1.839 0.58l2.607 0.643q1.607 0.393 2 0.643 0.571 0.357 0.571 1.071 0 0.696-0.714 1.152t-1.875 0.455q-0.911 0-1.634-0.286t-1.161-0.688-0.813-0.804-0.821-0.688-0.964-0.286q-0.893 0-1.348 0.536t-0.455 1.339q0 1.643 2.179 2.813t5.196 1.17q1.304 0 2.5-0.33t2.188-0.955 1.58-1.67 0.589-2.348zM27.429 22.857q0 2.839-2.009 4.848t-4.848 2.009q-2.321 0-4.179-1.429-1.375 0.286-2.679 0.286-2.554 0-4.884-0.991t-4.018-2.679-2.679-4.018-0.991-4.884q0-1.304 0.286-2.679-1.429-1.857-1.429-4.179 0-2.839 2.009-4.848t4.848-2.009q2.321 0 4.179 1.429 1.375-0.286 2.679-0.286 2.554 0 4.884 0.991t4.018 2.679 2.679 4.018 0.991 4.884q0 1.304-0.286 2.679 1.429 1.857 1.429 4.179z"></path> </symbol> <symbol id="icon-foursquare" viewBox="0 0 23 32"> <path class="path1" d="M17.857 7.75l0.661-3.464q0.089-0.411-0.161-0.714t-0.625-0.304h-12.714q-0.411 0-0.688 0.304t-0.277 0.661v19.661q0 0.125 0.107 0.018l5.196-6.286q0.411-0.464 0.679-0.598t0.857-0.134h4.268q0.393 0 0.661-0.259t0.321-0.527q0.429-2.321 0.661-3.411 0.071-0.375-0.205-0.714t-0.652-0.339h-5.25q-0.518 0-0.857-0.339t-0.339-0.857v-0.75q0-0.518 0.339-0.848t0.857-0.33h6.179q0.321 0 0.625-0.241t0.357-0.527zM21.911 3.786q-0.268 1.304-0.955 4.759t-1.241 6.25-0.625 3.098q-0.107 0.393-0.161 0.58t-0.25 0.58-0.438 0.589-0.688 0.375-1.036 0.179h-4.839q-0.232 0-0.393 0.179-0.143 0.161-7.607 8.821-0.393 0.446-1.045 0.509t-0.866-0.098q-0.982-0.393-0.982-1.75v-25.179q0-0.982 0.679-1.83t2.143-0.848h15.857q1.696 0 2.268 0.946t0.179 2.839zM21.911 3.786l-2.821 14.107q0.071-0.304 0.625-3.098t1.241-6.25 0.955-4.759z"></path> </symbol> <symbol id="icon-wordpress" viewBox="0 0 32 32"> <path class="path1" d="M2.268 16q0-2.911 1.196-5.589l6.554 17.946q-3.5-1.696-5.625-5.018t-2.125-7.339zM25.268 15.304q0 0.339-0.045 0.688t-0.179 0.884-0.205 0.786-0.313 1.054-0.313 1.036l-1.357 4.571-4.964-14.75q0.821-0.054 1.571-0.143 0.339-0.036 0.464-0.33t-0.045-0.554-0.509-0.241l-3.661 0.179q-1.339-0.018-3.607-0.179-0.214-0.018-0.366 0.089t-0.205 0.268-0.027 0.33 0.161 0.295 0.348 0.143l1.429 0.143 2.143 5.857-3 9-5-14.857q0.821-0.054 1.571-0.143 0.339-0.036 0.464-0.33t-0.045-0.554-0.509-0.241l-3.661 0.179q-0.125 0-0.411-0.009t-0.464-0.009q1.875-2.857 4.902-4.527t6.563-1.67q2.625 0 5.009 0.946t4.259 2.661h-0.179q-0.982 0-1.643 0.723t-0.661 1.705q0 0.214 0.036 0.429t0.071 0.384 0.143 0.411 0.161 0.375 0.214 0.402 0.223 0.375 0.259 0.429 0.25 0.411q1.125 1.911 1.125 3.786zM16.232 17.196l4.232 11.554q0.018 0.107 0.089 0.196-2.25 0.786-4.554 0.786-2 0-3.875-0.571zM28.036 9.411q1.696 3.107 1.696 6.589 0 3.732-1.857 6.884t-4.982 4.973l4.196-12.107q1.054-3.018 1.054-4.929 0-0.75-0.107-1.411zM16 0q3.25 0 6.214 1.268t5.107 3.411 3.411 5.107 1.268 6.214-1.268 6.214-3.411 5.107-5.107 3.411-6.214 1.268-6.214-1.268-5.107-3.411-3.411-5.107-1.268-6.214 1.268-6.214 3.411-5.107 5.107-3.411 6.214-1.268zM16 31.268q3.089 0 5.92-1.214t4.875-3.259 3.259-4.875 1.214-5.92-1.214-5.92-3.259-4.875-4.875-3.259-5.92-1.214-5.92 1.214-4.875 3.259-3.259 4.875-1.214 5.92 1.214 5.92 3.259 4.875 4.875 3.259 5.92 1.214z"></path> </symbol> <symbol id="icon-stumbleupon" viewBox="0 0 34 32"> <path class="path1" d="M18.964 12.714v-2.107q0-0.75-0.536-1.286t-1.286-0.536-1.286 0.536-0.536 1.286v10.929q0 3.125-2.25 5.339t-5.411 2.214q-3.179 0-5.42-2.241t-2.241-5.42v-4.75h5.857v4.679q0 0.768 0.536 1.295t1.286 0.527 1.286-0.527 0.536-1.295v-11.071q0-3.054 2.259-5.214t5.384-2.161q3.143 0 5.393 2.179t2.25 5.25v2.429l-3.482 1.036zM28.429 16.679h5.857v4.75q0 3.179-2.241 5.42t-5.42 2.241q-3.161 0-5.411-2.223t-2.25-5.366v-4.786l2.339 1.089 3.482-1.036v4.821q0 0.75 0.536 1.277t1.286 0.527 1.286-0.527 0.536-1.277v-4.911z"></path> </symbol> <symbol id="icon-digg" viewBox="0 0 37 32"> <path class="path1" d="M5.857 5.036h3.643v17.554h-9.5v-12.446h5.857v-5.107zM5.857 19.661v-6.589h-2.196v6.589h2.196zM10.964 10.143v12.446h3.661v-12.446h-3.661zM10.964 5.036v3.643h3.661v-3.643h-3.661zM16.089 10.143h9.518v16.821h-9.518v-2.911h5.857v-1.464h-5.857v-12.446zM21.946 19.661v-6.589h-2.196v6.589h2.196zM27.071 10.143h9.5v16.821h-9.5v-2.911h5.839v-1.464h-5.839v-12.446zM32.911 19.661v-6.589h-2.196v6.589h2.196z"></path> </symbol> <symbol id="icon-spotify" viewBox="0 0 27 32"> <path class="path1" d="M20.125 21.607q0-0.571-0.536-0.911-3.446-2.054-7.982-2.054-2.375 0-5.125 0.607-0.75 0.161-0.75 0.929 0 0.357 0.241 0.616t0.634 0.259q0.089 0 0.661-0.143 2.357-0.482 4.339-0.482 4.036 0 7.089 1.839 0.339 0.196 0.589 0.196 0.339 0 0.589-0.241t0.25-0.616zM21.839 17.768q0-0.714-0.625-1.089-4.232-2.518-9.786-2.518-2.732 0-5.411 0.75-0.857 0.232-0.857 1.143 0 0.446 0.313 0.759t0.759 0.313q0.125 0 0.661-0.143 2.179-0.589 4.482-0.589 4.982 0 8.714 2.214 0.429 0.232 0.679 0.232 0.446 0 0.759-0.313t0.313-0.759zM23.768 13.339q0-0.839-0.714-1.25-2.25-1.304-5.232-1.973t-6.125-0.67q-3.643 0-6.5 0.839-0.411 0.125-0.688 0.455t-0.277 0.866q0 0.554 0.366 0.929t0.92 0.375q0.196 0 0.714-0.143 2.375-0.661 5.482-0.661 2.839 0 5.527 0.607t4.527 1.696q0.375 0.214 0.714 0.214 0.518 0 0.902-0.366t0.384-0.92zM27.429 16q0 3.732-1.839 6.884t-4.991 4.991-6.884 1.839-6.884-1.839-4.991-4.991-1.839-6.884 1.839-6.884 4.991-4.991 6.884-1.839 6.884 1.839 4.991 4.991 1.839 6.884z"></path> </symbol> <symbol id="icon-soundcloud" viewBox="0 0 41 32"> <path class="path1" d="M14 24.5l0.286-4.304-0.286-9.339q-0.018-0.179-0.134-0.304t-0.295-0.125q-0.161 0-0.286 0.125t-0.125 0.304l-0.25 9.339 0.25 4.304q0.018 0.179 0.134 0.295t0.277 0.116q0.393 0 0.429-0.411zM19.286 23.982l0.196-3.768-0.214-10.464q0-0.286-0.232-0.429-0.143-0.089-0.286-0.089t-0.286 0.089q-0.232 0.143-0.232 0.429l-0.018 0.107-0.179 10.339q0 0.018 0.196 4.214v0.018q0 0.179 0.107 0.304 0.161 0.196 0.411 0.196 0.196 0 0.357-0.161 0.161-0.125 0.161-0.357zM0.625 17.911l0.357 2.286-0.357 2.25q-0.036 0.161-0.161 0.161t-0.161-0.161l-0.304-2.25 0.304-2.286q0.036-0.161 0.161-0.161t0.161 0.161zM2.161 16.5l0.464 3.696-0.464 3.625q-0.036 0.161-0.179 0.161-0.161 0-0.161-0.179l-0.411-3.607 0.411-3.696q0-0.161 0.161-0.161 0.143 0 0.179 0.161zM3.804 15.821l0.446 4.375-0.446 4.232q0 0.196-0.196 0.196-0.179 0-0.214-0.196l-0.375-4.232 0.375-4.375q0.036-0.214 0.214-0.214 0.196 0 0.196 0.214zM5.482 15.696l0.411 4.5-0.411 4.357q-0.036 0.232-0.25 0.232-0.232 0-0.232-0.232l-0.375-4.357 0.375-4.5q0-0.232 0.232-0.232 0.214 0 0.25 0.232zM7.161 16.018l0.375 4.179-0.375 4.393q-0.036 0.286-0.286 0.286-0.107 0-0.188-0.080t-0.080-0.205l-0.357-4.393 0.357-4.179q0-0.107 0.080-0.188t0.188-0.080q0.25 0 0.286 0.268zM8.839 13.411l0.375 6.786-0.375 4.393q0 0.125-0.089 0.223t-0.214 0.098q-0.286 0-0.321-0.321l-0.321-4.393 0.321-6.786q0.036-0.321 0.321-0.321 0.125 0 0.214 0.098t0.089 0.223zM10.518 11.875l0.339 8.357-0.339 4.357q0 0.143-0.098 0.241t-0.241 0.098q-0.321 0-0.357-0.339l-0.286-4.357 0.286-8.357q0.036-0.339 0.357-0.339 0.143 0 0.241 0.098t0.098 0.241zM12.268 11.161l0.321 9.036-0.321 4.321q-0.036 0.375-0.393 0.375-0.339 0-0.375-0.375l-0.286-4.321 0.286-9.036q0-0.161 0.116-0.277t0.259-0.116q0.161 0 0.268 0.116t0.125 0.277zM19.268 24.411v0 0zM15.732 11.089l0.268 9.107-0.268 4.268q0 0.179-0.134 0.313t-0.313 0.134-0.304-0.125-0.143-0.321l-0.25-4.268 0.25-9.107q0-0.196 0.134-0.321t0.313-0.125 0.313 0.125 0.134 0.321zM17.5 11.429l0.25 8.786-0.25 4.214q0 0.196-0.143 0.339t-0.339 0.143-0.339-0.143-0.161-0.339l-0.214-4.214 0.214-8.786q0.018-0.214 0.161-0.357t0.339-0.143 0.33 0.143 0.152 0.357zM21.286 20.214l-0.25 4.125q0 0.232-0.161 0.393t-0.393 0.161-0.393-0.161-0.179-0.393l-0.107-2.036-0.107-2.089 0.214-11.357v-0.054q0.036-0.268 0.214-0.429 0.161-0.125 0.357-0.125 0.143 0 0.268 0.089 0.25 0.143 0.286 0.464zM41.143 19.875q0 2.089-1.482 3.563t-3.571 1.473h-14.036q-0.232-0.036-0.393-0.196t-0.161-0.393v-16.054q0-0.411 0.5-0.589 1.518-0.607 3.232-0.607 3.482 0 6.036 2.348t2.857 5.777q0.946-0.393 1.964-0.393 2.089 0 3.571 1.482t1.482 3.589z"></path> </symbol> <symbol id="icon-codepen" viewBox="0 0 32 32"> <path class="path1" d="M3.857 20.875l10.768 7.179v-6.411l-5.964-3.982zM2.75 18.304l3.446-2.304-3.446-2.304v4.607zM17.375 28.054l10.768-7.179-4.804-3.214-5.964 3.982v6.411zM16 19.25l4.857-3.25-4.857-3.25-4.857 3.25zM8.661 14.339l5.964-3.982v-6.411l-10.768 7.179zM25.804 16l3.446 2.304v-4.607zM23.339 14.339l4.804-3.214-10.768-7.179v6.411zM32 11.125v9.75q0 0.732-0.607 1.143l-14.625 9.75q-0.375 0.232-0.768 0.232t-0.768-0.232l-14.625-9.75q-0.607-0.411-0.607-1.143v-9.75q0-0.732 0.607-1.143l14.625-9.75q0.375-0.232 0.768-0.232t0.768 0.232l14.625 9.75q0.607 0.411 0.607 1.143z"></path> </symbol> <symbol id="icon-twitch" viewBox="0 0 32 32"> <path class="path1" d="M16 7.75v7.75h-2.589v-7.75h2.589zM23.107 7.75v7.75h-2.589v-7.75h2.589zM23.107 21.321l4.518-4.536v-14.196h-21.321v18.732h5.821v3.875l3.875-3.875h7.107zM30.214 0v18.089l-7.75 7.75h-5.821l-3.875 3.875h-3.875v-3.875h-7.107v-20.679l1.946-5.161h26.482z"></path> </symbol> <symbol id="icon-meanpath" viewBox="0 0 27 32"> <path class="path1" d="M23.411 15.036v2.036q0 0.429-0.241 0.679t-0.67 0.25h-3.607q-0.429 0-0.679-0.25t-0.25-0.679v-2.036q0-0.429 0.25-0.679t0.679-0.25h3.607q0.429 0 0.67 0.25t0.241 0.679zM14.661 19.143v-4.464q0-0.946-0.58-1.527t-1.527-0.58h-2.375q-1.214 0-1.714 0.929-0.5-0.929-1.714-0.929h-2.321q-0.946 0-1.527 0.58t-0.58 1.527v4.464q0 0.393 0.375 0.393h0.982q0.393 0 0.393-0.393v-4.107q0-0.429 0.241-0.679t0.688-0.25h1.679q0.429 0 0.679 0.25t0.25 0.679v4.107q0 0.393 0.375 0.393h0.964q0.393 0 0.393-0.393v-4.107q0-0.429 0.25-0.679t0.679-0.25h1.732q0.429 0 0.67 0.25t0.241 0.679v4.107q0 0.393 0.393 0.393h0.982q0.375 0 0.375-0.393zM25.179 17.429v-2.75q0-0.946-0.589-1.527t-1.536-0.58h-4.714q-0.946 0-1.536 0.58t-0.589 1.527v7.321q0 0.375 0.393 0.375h0.982q0.375 0 0.375-0.375v-3.214q0.554 0.75 1.679 0.75h3.411q0.946 0 1.536-0.58t0.589-1.527zM27.429 6.429v19.143q0 1.714-1.214 2.929t-2.929 1.214h-19.143q-1.714 0-2.929-1.214t-1.214-2.929v-19.143q0-1.714 1.214-2.929t2.929-1.214h19.143q1.714 0 2.929 1.214t1.214 2.929z"></path> </symbol> <symbol id="icon-pinterest-p" viewBox="0 0 23 32"> <path class="path1" d="M0 10.661q0-1.929 0.67-3.634t1.848-2.973 2.714-2.196 3.304-1.393 3.607-0.464q2.821 0 5.25 1.188t3.946 3.455 1.518 5.125q0 1.714-0.339 3.357t-1.071 3.161-1.786 2.67-2.589 1.839-3.375 0.688q-1.214 0-2.411-0.571t-1.714-1.571q-0.179 0.696-0.5 2.009t-0.42 1.696-0.366 1.268-0.464 1.268-0.571 1.116-0.821 1.384-1.107 1.545l-0.25 0.089-0.161-0.179q-0.268-2.804-0.268-3.357 0-1.643 0.384-3.688t1.188-5.134 0.929-3.625q-0.571-1.161-0.571-3.018 0-1.482 0.929-2.786t2.357-1.304q1.089 0 1.696 0.723t0.607 1.83q0 1.179-0.786 3.411t-0.786 3.339q0 1.125 0.804 1.866t1.946 0.741q0.982 0 1.821-0.446t1.402-1.214 1-1.696 0.679-1.973 0.357-1.982 0.116-1.777q0-3.089-1.955-4.813t-5.098-1.723q-3.571 0-5.964 2.313t-2.393 5.866q0 0.786 0.223 1.518t0.482 1.161 0.482 0.813 0.223 0.545q0 0.5-0.268 1.304t-0.661 0.804q-0.036 0-0.304-0.054-0.911-0.268-1.616-1t-1.089-1.688-0.58-1.929-0.196-1.902z"></path> </symbol> <symbol id="icon-get-pocket" viewBox="0 0 31 32"> <path class="path1" d="M27.946 2.286q1.161 0 1.964 0.813t0.804 1.973v9.268q0 3.143-1.214 6t-3.259 4.911-4.893 3.259-5.973 1.205q-3.143 0-5.991-1.205t-4.902-3.259-3.268-4.911-1.214-6v-9.268q0-1.143 0.821-1.964t1.964-0.821h25.161zM15.375 21.286q0.839 0 1.464-0.589l7.214-6.929q0.661-0.625 0.661-1.518 0-0.875-0.616-1.491t-1.491-0.616q-0.839 0-1.464 0.589l-5.768 5.536-5.768-5.536q-0.625-0.589-1.446-0.589-0.875 0-1.491 0.616t-0.616 1.491q0 0.911 0.643 1.518l7.232 6.929q0.589 0.589 1.446 0.589z"></path> </symbol> <symbol id="icon-vimeo" viewBox="0 0 32 32"> <path class="path1" d="M30.518 9.25q-0.179 4.214-5.929 11.625-5.946 7.696-10.036 7.696-2.536 0-4.286-4.696-0.786-2.857-2.357-8.607-1.286-4.679-2.804-4.679-0.321 0-2.268 1.357l-1.375-1.75q0.429-0.375 1.929-1.723t2.321-2.063q2.786-2.464 4.304-2.607 1.696-0.161 2.732 0.991t1.446 3.634q0.786 5.125 1.179 6.661 0.982 4.446 2.143 4.446 0.911 0 2.75-2.875 1.804-2.875 1.946-4.393 0.232-2.482-1.946-2.482-1.018 0-2.161 0.464 2.143-7.018 8.196-6.821 4.482 0.143 4.214 5.821z"></path> </symbol> <symbol id="icon-reddit-alien" viewBox="0 0 32 32"> <path class="path1" d="M32 15.107q0 1.036-0.527 1.884t-1.42 1.295q0.214 0.821 0.214 1.714 0 2.768-1.902 5.125t-5.188 3.723-7.143 1.366-7.134-1.366-5.179-3.723-1.902-5.125q0-0.839 0.196-1.679-0.911-0.446-1.464-1.313t-0.554-1.902q0-1.464 1.036-2.509t2.518-1.045q1.518 0 2.589 1.125 3.893-2.714 9.196-2.893l2.071-9.304q0.054-0.232 0.268-0.375t0.464-0.089l6.589 1.446q0.321-0.661 0.964-1.063t1.411-0.402q1.107 0 1.893 0.777t0.786 1.884-0.786 1.893-1.893 0.786-1.884-0.777-0.777-1.884l-5.964-1.321-1.857 8.429q5.357 0.161 9.268 2.857 1.036-1.089 2.554-1.089 1.482 0 2.518 1.045t1.036 2.509zM7.464 18.661q0 1.107 0.777 1.893t1.884 0.786 1.893-0.786 0.786-1.893-0.786-1.884-1.893-0.777q-1.089 0-1.875 0.786t-0.786 1.875zM21.929 25q0.196-0.196 0.196-0.464t-0.196-0.464q-0.179-0.179-0.446-0.179t-0.464 0.179q-0.732 0.75-2.161 1.107t-2.857 0.357-2.857-0.357-2.161-1.107q-0.196-0.179-0.464-0.179t-0.446 0.179q-0.196 0.179-0.196 0.455t0.196 0.473q0.768 0.768 2.116 1.214t2.188 0.527 1.625 0.080 1.625-0.080 2.188-0.527 2.116-1.214zM21.875 21.339q1.107 0 1.884-0.786t0.777-1.893q0-1.089-0.786-1.875t-1.875-0.786q-1.107 0-1.893 0.777t-0.786 1.884 0.786 1.893 1.893 0.786z"></path> </symbol> <symbol id="icon-hashtag" viewBox="0 0 32 32"> <path class="path1" d="M17.696 18.286l1.143-4.571h-4.536l-1.143 4.571h4.536zM31.411 9.286l-1 4q-0.125 0.429-0.554 0.429h-5.839l-1.143 4.571h5.554q0.268 0 0.446 0.214 0.179 0.25 0.107 0.5l-1 4q-0.089 0.429-0.554 0.429h-5.839l-1.446 5.857q-0.125 0.429-0.554 0.429h-4q-0.286 0-0.464-0.214-0.161-0.214-0.107-0.5l1.393-5.571h-4.536l-1.446 5.857q-0.125 0.429-0.554 0.429h-4.018q-0.268 0-0.446-0.214-0.161-0.214-0.107-0.5l1.393-5.571h-5.554q-0.268 0-0.446-0.214-0.161-0.214-0.107-0.5l1-4q0.125-0.429 0.554-0.429h5.839l1.143-4.571h-5.554q-0.268 0-0.446-0.214-0.179-0.25-0.107-0.5l1-4q0.089-0.429 0.554-0.429h5.839l1.446-5.857q0.125-0.429 0.571-0.429h4q0.268 0 0.446 0.214 0.161 0.214 0.107 0.5l-1.393 5.571h4.536l1.446-5.857q0.125-0.429 0.571-0.429h4q0.268 0 0.446 0.214 0.161 0.214 0.107 0.5l-1.393 5.571h5.554q0.268 0 0.446 0.214 0.161 0.214 0.107 0.5z"></path> </symbol> <symbol id="icon-chain" viewBox="0 0 30 32"> <path class="path1" d="M26 21.714q0-0.714-0.5-1.214l-3.714-3.714q-0.5-0.5-1.214-0.5-0.75 0-1.286 0.571 0.054 0.054 0.339 0.33t0.384 0.384 0.268 0.339 0.232 0.455 0.063 0.491q0 0.714-0.5 1.214t-1.214 0.5q-0.268 0-0.491-0.063t-0.455-0.232-0.339-0.268-0.384-0.384-0.33-0.339q-0.589 0.554-0.589 1.304 0 0.714 0.5 1.214l3.679 3.696q0.482 0.482 1.214 0.482 0.714 0 1.214-0.464l2.625-2.607q0.5-0.5 0.5-1.196zM13.446 9.125q0-0.714-0.5-1.214l-3.679-3.696q-0.5-0.5-1.214-0.5-0.696 0-1.214 0.482l-2.625 2.607q-0.5 0.5-0.5 1.196 0 0.714 0.5 1.214l3.714 3.714q0.482 0.482 1.214 0.482 0.75 0 1.286-0.554-0.054-0.054-0.339-0.33t-0.384-0.384-0.268-0.339-0.232-0.455-0.063-0.491q0-0.714 0.5-1.214t1.214-0.5q0.268 0 0.491 0.063t0.455 0.232 0.339 0.268 0.384 0.384 0.33 0.339q0.589-0.554 0.589-1.304zM29.429 21.714q0 2.143-1.518 3.625l-2.625 2.607q-1.482 1.482-3.625 1.482-2.161 0-3.643-1.518l-3.679-3.696q-1.482-1.482-1.482-3.625 0-2.196 1.571-3.732l-1.571-1.571q-1.536 1.571-3.714 1.571-2.143 0-3.643-1.5l-3.714-3.714q-1.5-1.5-1.5-3.643t1.518-3.625l2.625-2.607q1.482-1.482 3.625-1.482 2.161 0 3.643 1.518l3.679 3.696q1.482 1.482 1.482 3.625 0 2.196-1.571 3.732l1.571 1.571q1.536-1.571 3.714-1.571 2.143 0 3.643 1.5l3.714 3.714q1.5 1.5 1.5 3.643z"></path> </symbol> <symbol id="icon-thumb-tack" viewBox="0 0 21 32"> <path class="path1" d="M8.571 15.429v-8q0-0.25-0.161-0.411t-0.411-0.161-0.411 0.161-0.161 0.411v8q0 0.25 0.161 0.411t0.411 0.161 0.411-0.161 0.161-0.411zM20.571 21.714q0 0.464-0.339 0.804t-0.804 0.339h-7.661l-0.911 8.625q-0.036 0.214-0.188 0.366t-0.366 0.152h-0.018q-0.482 0-0.571-0.482l-1.357-8.661h-7.214q-0.464 0-0.804-0.339t-0.339-0.804q0-2.196 1.402-3.955t3.17-1.759v-9.143q-0.929 0-1.607-0.679t-0.679-1.607 0.679-1.607 1.607-0.679h11.429q0.929 0 1.607 0.679t0.679 1.607-0.679 1.607-1.607 0.679v9.143q1.768 0 3.17 1.759t1.402 3.955z"></path> </symbol> <symbol id="icon-arrow-left" viewBox="0 0 43 32"> <path class="path1" d="M42.311 14.044c-0.178-0.178-0.533-0.356-0.711-0.356h-33.778l10.311-10.489c0.178-0.178 0.356-0.533 0.356-0.711 0-0.356-0.178-0.533-0.356-0.711l-1.6-1.422c-0.356-0.178-0.533-0.356-0.889-0.356s-0.533 0.178-0.711 0.356l-14.578 14.933c-0.178 0.178-0.356 0.533-0.356 0.711s0.178 0.533 0.356 0.711l14.756 14.933c0 0.178 0.356 0.356 0.533 0.356s0.533-0.178 0.711-0.356l1.6-1.6c0.178-0.178 0.356-0.533 0.356-0.711s-0.178-0.533-0.356-0.711l-10.311-10.489h33.778c0.178 0 0.533-0.178 0.711-0.356 0.356-0.178 0.533-0.356 0.533-0.711v-2.133c0-0.356-0.178-0.711-0.356-0.889z"></path> </symbol> <symbol id="icon-arrow-right" viewBox="0 0 43 32"> <path class="path1" d="M0.356 17.956c0.178 0.178 0.533 0.356 0.711 0.356h33.778l-10.311 10.489c-0.178 0.178-0.356 0.533-0.356 0.711 0 0.356 0.178 0.533 0.356 0.711l1.6 1.6c0.178 0.178 0.533 0.356 0.711 0.356s0.533-0.178 0.711-0.356l14.756-14.933c0.178-0.356 0.356-0.711 0.356-0.889s-0.178-0.533-0.356-0.711l-14.756-14.933c0-0.178-0.356-0.356-0.533-0.356s-0.533 0.178-0.711 0.356l-1.6 1.6c-0.178 0.178-0.356 0.533-0.356 0.711s0.178 0.533 0.356 0.711l10.311 10.489h-33.778c-0.178 0-0.533 0.178-0.711 0.356-0.356 0.178-0.533 0.356-0.533 0.711v2.311c0 0.178 0.178 0.533 0.356 0.711z"></path> </symbol> <symbol id="icon-play" viewBox="0 0 22 28"> <path d="M21.625 14.484l-20.75 11.531c-0.484 0.266-0.875 0.031-0.875-0.516v-23c0-0.547 0.391-0.781 0.875-0.516l20.75 11.531c0.484 0.266 0.484 0.703 0 0.969z"></path> </symbol> <symbol id="icon-pause" viewBox="0 0 24 28"> <path d="M24 3v22c0 0.547-0.453 1-1 1h-8c-0.547 0-1-0.453-1-1v-22c0-0.547 0.453-1 1-1h8c0.547 0 1 0.453 1 1zM10 3v22c0 0.547-0.453 1-1 1h-8c-0.547 0-1-0.453-1-1v-22c0-0.547 0.453-1 1-1h8c0.547 0 1 0.453 1 1z"></path> </symbol> </defs> </svg> </body> </html> <!-- Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/ Object Caching 185/251 objects using Redis Page Caching using Redis Fragment Caching 1/8 fragments using Redis Served from: localhost @ 2024-11-26 15:30:20 by W3 Total Cache -->

Pages: 1 2 3 4 5 6 7 8 9 10