<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Cloudflight Engineering Blog]]></title><description><![CDATA[Cloudflight Engineering Blog]]></description><link>https://engineering.cloudflight.io</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1651840848260/Lykf4cLJC.png</url><title>Cloudflight Engineering Blog</title><link>https://engineering.cloudflight.io</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 17:46:53 GMT</lastBuildDate><atom:link href="https://engineering.cloudflight.io/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to setup Universal Links for iOS in React Native]]></title><description><![CDATA[TL;DR
This post explains how iOS Universal Links work and what is required to implement them. It covers how to handle incoming links in an iOS app, how to create and configure the Apple App Site Association (AASA) file, and how to correctly host the ...]]></description><link>https://engineering.cloudflight.io/how-to-setup-universal-links-for-ios-in-react-native</link><guid isPermaLink="true">https://engineering.cloudflight.io/how-to-setup-universal-links-for-ios-in-react-native</guid><category><![CDATA[universal links]]></category><category><![CDATA[iOS]]></category><category><![CDATA[React Native]]></category><dc:creator><![CDATA[Dragos Neghina]]></dc:creator><pubDate>Wed, 21 Jan 2026 07:22:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769081180270/227e1236-d522-4a9e-b047-ab7f62c62e8f.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr"><strong>TL;DR</strong></h1>
<p>This post explains how iOS <strong>Universal Links</strong> work and what is required to implement them. It covers how to handle incoming links in an iOS app, how to create and configure the <strong>Apple App Site Association (AASA) file</strong>, and how to correctly host the file so that iOS can securely associate a domain with an app and route matching URLs from the web into the app.</p>
<h2 id="heading-who-should-read-the-post"><strong>Who should read the post?</strong></h2>
<ul>
<li><p><strong>Software Developers</strong> who plan to integrate this feature into their app.</p>
</li>
<li><p><strong>People in tech</strong> with little to no experience on <strong>Universal Links</strong> who want to learn something new.</p>
</li>
</ul>
<hr />
<h1 id="heading-introduction"><strong>Introduction</strong></h1>
<p>Have you ever tapped on an ad or email link and been taken directly to the desired application without seeing a website or pop-up asking for permission? That seamless transition from the web to the app isn't magic; it's powered by <strong>Universal Links</strong>.</p>
<p><strong>Universal Links</strong> allow iOS (or Android) to open your app automatically when a user taps a specific URL belonging to a domain you control. If the app is installed, the link opens the app. If it isn't, the same link simply opens in the browser. This behaviour becomes a powerful tool for ads, emails, deep navigation, onboarding flows, and the overall user experience.</p>
<p>Despite how seamless they feel to users, it requires careful configuration across your iOS app and backend. For developers encountering them for the first time, the setup process can be surprisingly strict and easy to misconfigure.</p>
<p>This post will cover the steps required to make <strong>Universal Links</strong> work on iOS.</p>
<ul>
<li><p>Configure your iOS app to handle incoming links.</p>
</li>
<li><p>Creating the <strong>Apple App Site Association</strong> <strong>(AASA) file</strong>.</p>
</li>
<li><p>Hosting the <strong>AASA file</strong> under the <strong>/.well-known</strong> path.</p>
</li>
</ul>
<p>By the end, you will have a better understanding of how to set up an application to support <strong>Universal Links</strong>, which will give you a head start if you plan on integrating this feature into your software.</p>
<hr />
<h1 id="heading-configuring-your-ios-app-to-handle-incoming-links"><strong>Configuring your iOS app to handle incoming links</strong></h1>
<p>The first step is ensuring that your iOS app knows how to react when someone taps a <strong>Universal Link</strong>. Although Universal Links originate on the web, iOS redirects them to the app via the <strong>App Delegate</strong>. This makes the <strong>App Delegate</strong> the central point where your app decides how to handle an <strong>i</strong>ncoming URL. Options include opening a specific screen, triggering navigation, or passing parameters deeper into your <strong>React Native</strong> layer.</p>
<p>When a <strong>Universal Link</strong> is activated, iOS calls two native methods:</p>
<ul>
<li><p><code>scene(_:continue:)</code> for apps using the modern Scenes API</p>
</li>
<li><p><code>application(_:continue:restorationHandler:)</code> for older setups</p>
</li>
</ul>
<p>Your job is to implement at least one of these and route the received URL into your code.</p>
<p>Here’s a simplified example using the <strong>SceneDelegate</strong>, which is common in <strong>React Native</strong> projects:</p>
<pre><code class="lang-swift"><span class="hljs-comment">// SceneDelegate.swift</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">scene</span><span class="hljs-params">(<span class="hljs-number">_</span> scene: UIScene, <span class="hljs-keyword">continue</span> userActivity: NSUserActivity)</span></span> {
    <span class="hljs-keyword">guard</span> userActivity.activityType == <span class="hljs-type">NSUserActivityTypeBrowsingWeb</span>,
          <span class="hljs-keyword">let</span> incomingURL = userActivity.webpageURL <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> }

    <span class="hljs-comment">// Pass the link to React Native</span>
    <span class="hljs-type">RCTLinkingManager</span>.application(<span class="hljs-type">UIApplication</span>.shared,
                                  <span class="hljs-keyword">open</span>: incomingURL,
                                  options: [:])
}
</code></pre>
<p>If your project still relies on the <strong>AppDelegate</strong>, the equivalent version looks like:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// AppDelegate.m (Objective-C)</span>
- (BOOL)application:(UIApplication *)application
            continueUserActivity:(NSUserActivity *)userActivity
              restorationHandler:(<span class="hljs-built_in">void</span> (^)(NSArray *))restorationHandler
{
  <span class="hljs-keyword">return</span> [RCTLinkingManager application:application
                      continueUserActivity:userActivity
                        restorationHandler:restorationHandler];
}
</code></pre>
<p>This implementation ensures your <strong>React Nativ</strong>e app receives the link through the Linking API, where you can listen for it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Linking } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-native'</span>;

Linking.addEventListener(<span class="hljs-string">'url'</span>, <span class="hljs-function">(<span class="hljs-params">{ url }</span>) =&gt;</span> {
  <span class="hljs-comment">// Handle navigation here</span>
});
</code></pre>
<p>At this point, the native layer is ready — your app can now receive <strong>Universal Links</strong>.<br />Next, we’ll prepare the website side of the handshake by creating the <strong>AASA file</strong> so iOS knows your app is authorised to handle those links.</p>
<hr />
<h1 id="heading-creating-the-apple-app-site-association-aasa-file"><strong>Creating the Apple App Site Association (AASA) File</strong></h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768901210758/cebfa559-aabf-4a15-a8a4-f2bb4fb96bb9.png" alt /></p>
<p>Now that the app can receive <strong>Universal Links</strong>, the next step is to convince iOS that your domain is allowed to open your app. This is where the <strong>AASA file</strong> comes in.</p>
<p>At a high level, the <strong>AASA file</strong> is a JSON document on your website that tells iOS:</p>
<ul>
<li><p>which app(s) belong to your domain</p>
</li>
<li><p>which URL paths those apps are allowed to handle</p>
</li>
<li><p>whether you support features like Shared Web Credentials or <strong>Universal Links</strong></p>
</li>
</ul>
<p>When a user installs your app, iOS automatically fetches the file from your domain. If the file is valid and correctly formatted and located in the expected place, iOS registers your app as the handler for all matching paths.</p>
<hr />
<h2 id="heading-aasa-file-structure"><strong>AASA File Structure</strong></h2>
<p>Below is the minimal structure iOS expects in an <code>apple-app-site-association</code> file and how each section influences link handling:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"applinks"</span>: {
    <span class="hljs-attr">"details"</span>: [
      {
        <span class="hljs-attr">"appIDs"</span>: [<span class="hljs-string">"ABCDE12345.com.example.app"</span>],
        <span class="hljs-attr">"paths"</span>: [<span class="hljs-string">"/products/*"</span>, <span class="hljs-string">"/profile/*"</span>]
      }
    ]
  }
}
</code></pre>
<p>The <code>paths</code> are obviously the paths from the app that should trigger the <strong>universal link</strong> behaviour.</p>
<p>The <strong>appID</strong> is formed by: <code>&lt;Application Identifier Prefix&gt;.&lt;Bundle Identifier&gt;</code>.<br />The <code>Application Identifier Prefix</code> is visible in the <strong>Apple Developer</strong> portal under Membership or in <strong>Xcode</strong> by choosing your project → Signing &amp; Capabilities → selecting your team. The <code>Bundle Identifier</code> comes from the same <strong>Xcode</strong> section and uniquely identifies your app (e.g., <a target="_blank" href="http://com.company.app/"><strong>com.example.app</strong></a>).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768901785821/c9ed2ad9-77d7-417b-9313-4fcfdad709d5.png" alt /></p>
<hr />
<h2 id="heading-add-the-associated-domains-entitlement-to-your-app"><strong>Add the Associated Domains Entitlement to Your App</strong></h2>
<p>iOS will only validate the <strong>AASA file</strong> if your app explicitly declares the <strong>domain</strong> <strong>associated</strong> with it. To do this, enable the <strong>Associated Domains</strong> capability in <strong>Xcode</strong> and add an entry such as:</p>
<pre><code class="lang-json">applinks:&lt;your-domain&gt;.com
</code></pre>
<p>This links your app to the domain so iOS knows where to retrieve and verify your <strong>AASA file.</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768908058551/38350c5b-4265-4764-8c9f-20f1717bb84c.png" alt /></p>
<hr />
<h1 id="heading-hosting-the-aasa-file-under-the-well-known-path"><strong>Hosting the AASA File under the</strong> <code>/.well-known</code> path.</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768903303505/7731ffef-b0fb-4ffd-92a4-fb5a5bc4d130.png" alt /></p>
<p>For iOS to recognise your <strong>Universal Links</strong>, the <strong>AASA file</strong> must be publicly accessible at:</p>
<pre><code class="lang-xml">https://your-domain.com/.well-known/apple-app-site-association
</code></pre>
<p>iOS automatically fetches this file during app installation to verify the domain-app association. Hosting it in any other path or behind redirects will cause verification to fail, so serving it correctly is critical for <strong>Universal Links</strong> to work.</p>
<p>The file must be served directly, without any HTTP redirects (including <code>301</code>, <code>302</code>, or <code>307</code>). iOS will not follow redirects when attempting to fetch the <strong>AASA file</strong>, and even a redirect from <code>http</code> to <code>https</code> can cause the verification to fail.</p>
<p>Additionally, the response must be served with the correct <strong>Content-Type</strong> header:</p>
<pre><code class="lang-xml">Content-Type: application/json
</code></pre>
<p>The <strong>AASA file</strong> must be accessible over <strong>HTTPS</strong>, contain valid JSON, and be served as a raw file—<strong>not</strong> wrapped in HTML, compressed, or generated dynamically in a way that alters the response headers.</p>
<p>Because the requirements are strict and vary depending on your hosting provider or <strong>backend setup</strong> (e.g. Nginx, Apache, S3, Firebase Hosting, or a custom server), it’s easy to get this step wrong even if the file itself is correct.</p>
<p>In later posts, we’ll walk through concrete, step-by-step examples of hosting the <strong>AASA file</strong> correctly on different infrastructures and how to debug common issues when iOS fails to recognise your <strong>Universal Links</strong>.</p>
<hr />
<h1 id="heading-conclusion"><strong>Conclusion</strong></h1>
<p>Although <strong>Universal Links</strong> can seamlessly transition users from the web to your app, they only work when all the necessary components are correctly configured. Once your app and domain are properly associated, iOS can reliably determine whether to open your app or fall back to the browser when a link is clicked.</p>
<p>This post focuses on the core concepts needed to understand how <strong>Universal Links</strong> work. Follow-up posts will provide concrete setup examples and highlight common mistakes to avoid when implementing them.</p>
]]></content:encoded></item><item><title><![CDATA[Choosing the Right Test Automation Design Pattern: Page Object Model, Flow Model Pattern, or Screenplay Pattern?]]></title><description><![CDATA[TL;DR
The Page Object Model (POM) is the industry standard, but it is not easily scalable. The Flow Model Pattern improves upon the basic POM, offering a perfectly balanced solution that is not overly complex yet still scalable. The Screenplay Patter...]]></description><link>https://engineering.cloudflight.io/choosing-the-right-test-automation-design-pattern-page-object-model-flow-model-pattern-or-screenplay-pattern</link><guid isPermaLink="true">https://engineering.cloudflight.io/choosing-the-right-test-automation-design-pattern-page-object-model-flow-model-pattern-or-screenplay-pattern</guid><category><![CDATA[test-automation]]></category><category><![CDATA[test automation framework]]></category><category><![CDATA[Testing]]></category><category><![CDATA[testing framework]]></category><category><![CDATA[end to end testing]]></category><category><![CDATA[End-to-End]]></category><category><![CDATA[Software Testing]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Tomasz Buga]]></dc:creator><pubDate>Tue, 16 Dec 2025 10:40:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/26MJGnCM0Wc/upload/87645c69804169276568e03da5e0c24b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-tldr">TL;DR</h1>
<p>The <strong>Page Object Model</strong> <strong>(POM)</strong> is the industry standard, but it is not easily scalable. The <strong>Flow Model Pattern</strong> improves upon the basic <strong>POM</strong>, offering a perfectly balanced solution that is not overly complex yet still scalable. The <strong>Screenplay Pattern</strong> is best suited for highly complex solutions and requires test engineers to have more technical knowledge. Therefore, before implementing it, ensure that your team can maintain a more abstract framework. If that’s not possible, either hold programming workshops or stick with the <strong>Flow Model Pattern</strong>.</p>
<h1 id="heading-preface">Preface</h1>
<h2 id="heading-who-should-read-this-article"><strong>Who should read this article?</strong></h2>
<ul>
<li><p>Test Automation Engineers, Technical Architects, and Software Developers seeking optimal end-to-end test automation solutions</p>
</li>
<li><p>Anyone involved or interested in developing an end-to-end test automation strategy (e.g. Test managers, Test Automation Engineers, Technical Architects and Software Developers).</p>
</li>
</ul>
<h2 id="heading-what-is-this-article-about">What is this article about?</h2>
<p>After years of working with different test automation frameworks, I've learned that choosing the right design pattern isn't about finding the <strong><em>best</em></strong> <strong><em>one</em></strong>—it's about finding the <strong><em>right</em></strong> <strong><em>fit</em></strong> for your project and team.</p>
<p>The three primary patterns are:</p>
<ul>
<li><p><a target="_blank" href="https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/">Page Object Model</a></p>
</li>
<li><p><a target="_blank" href="https://www.peterfoldhazi.com/flow-model-pattern">Flow Model Pattern</a> (or <a target="_blank" href="https://www.browserstack.com/guide/design-patterns-in-automation-framework#toc3">Facade Design Pattern</a>)</p>
</li>
<li><p><a target="_blank" href="https://serenity-js.org/handbook/design/screenplay-pattern/">Screenplay Pattern</a></p>
</li>
</ul>
<p>This article explores when each pattern is most appropriate, drawing from my hands-on experience building and maintaining automation frameworks at different scales. I'll start with an overview of each approach, then guide you toward choosing the pattern that best fits your needs.</p>
<hr />
<h1 id="heading-api-based-model-a-universal-best-practice"><strong>API-Based Model: A Universal Best Practice</strong></h1>
<p>Before exploring different design patterns, I want to highlight a principle that applies to all of them: <strong>the API-based approach</strong> to test automation.</p>
<p>My colleague <strong>Jovan Ilić</strong> advocates for this in his article <em>"</em><a target="_blank" href="https://engineering.cloudflight.io/test-automation-api-based-model"><em>Test Automation: API-based Model</em></a><em>"</em>, and I strongly recommend it regardless of which pattern you choose.</p>
<h3 id="heading-the-core-principle"><strong>The Core Principle</strong></h3>
<blockquote>
<p><strong>Perform any action in the UI once, every other time use the API</strong></p>
</blockquote>
<p>Validate that the interface works, but use faster, more reliable API calls for test setup, navigation, and state management in subsequent tests.</p>
<h3 id="heading-why-it-matters"><strong>Why It Matters</strong></h3>
<p>This approach delivers significant benefits with any pattern:</p>
<ul>
<li><p>Faster test execution</p>
</li>
<li><p>Reduced flakiness</p>
</li>
<li><p>Better test isolation</p>
</li>
<li><p>Easier maintenance</p>
</li>
<li><p>More focused UI coverage</p>
</li>
</ul>
<p>Whether you use <strong>Page Object Model, Flow Model Pattern,</strong> or <strong>Screenplay Pattern,</strong> consider which interactions genuinely require UI validation and which can be handled more efficiently through APIs. This pragmatic approach will improve your test suite regardless of the architectural pattern you choose.</p>
<hr />
<h1 id="heading-page-object-model">Page Object Model</h1>
<h2 id="heading-overview">Overview</h2>
<p>The <a target="_blank" href="https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/"><strong>Page Object Model</strong> <strong>(POM)</strong></a> is the foundational design pattern that most test automation engineers learn first. The core idea is simple: separate page interaction logic from test scripts by encapsulating UI elements and their actions into dedicated page classes/files.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765358513151/5ded68f9-4cfc-4cc3-83aa-1e40165d6a67.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-description">Description</h2>
<p>The <strong>Page Object Model</strong> remains my preferred starting point for most projects—because of its proven effectiveness and universal recognition. This widespread adoption significantly shortens the ramp-up period for newcomers, as they can quickly adapt to a new codebase without learning an entirely new architectural approach.</p>
<p>While <strong>POM</strong> has its limitations (particularly as applications grow in complexity) its simplicity and directness make it an excellent foundation. Think of it as the baseline: straightforward to implement, easy to understand, and flexible enough to evolve as your project's needs change.</p>
<h2 id="heading-code-example">Code Example</h2>
<pre><code class="lang-javascript"><span class="hljs-comment">// For the sake of simplicity and framework-agnosticity</span>
<span class="hljs-comment">// we're skipping the selectors/locators definitions and functions</span>
<span class="hljs-comment">// that are mentioned on the picture, but not used within test case</span>

<span class="hljs-comment">// ===========</span>
<span class="hljs-comment">// Test Script</span>
<span class="hljs-comment">// ===========</span>
it(<span class="hljs-string">'should add yellow t-shirt to cart'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> ProductsPage.openProductDetails(<span class="hljs-string">'tshirt-yellow-medium'</span>);
    <span class="hljs-keyword">await</span> ProductDetailsPage.clickAddButton();

    <span class="hljs-keyword">const</span> cartCount = <span class="hljs-keyword">await</span> ProductDetailsPage.getCartCount();
    expect(cartCount).toBe(<span class="hljs-number">1</span>);
});

<span class="hljs-comment">// =============</span>
<span class="hljs-comment">// Products Page</span>
<span class="hljs-comment">// =============</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ProductsPage = {
    <span class="hljs-keyword">async</span> openProductDetails(id) { ... }
}

<span class="hljs-comment">// ====================</span>
<span class="hljs-comment">// Product Details Page</span>
<span class="hljs-comment">// ====================</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ProductDetailsPage = {
    <span class="hljs-keyword">async</span> clickAddButton() { ... },
    <span class="hljs-keyword">async</span> getCartCount() { ... }
}
</code></pre>
<h2 id="heading-summary">Summary</h2>
<p>✅ <strong>PROS</strong></p>
<ul>
<li><p>Widely adopted standard – most common pattern in test automation</p>
</li>
<li><p>Very intuitive – easy to understand for newcomers</p>
</li>
<li><p>Strong community support – abundant tutorials, examples, and help available</p>
</li>
<li><p>Flexible across all testing phases – DEV, SIT, UAT, etc.</p>
</li>
<li><p>Reduces code duplication – reusable page classes across multiple tests</p>
</li>
<li><p>Good separation of concerns – test logic separate from locators and interaction logic</p>
</li>
</ul>
<p>❌ <strong>DOWNSIDES</strong></p>
<ul>
<li><p>In pure form can get hard to maintain – repetitive code, page classes can become bloated (hundreds/thousands of lines)</p>
</li>
<li><p>Violates SOLID principles – particularly Single Responsibility Principle</p>
</li>
<li><p>Less efficient for applications where API testing would be more appropriate than UI testing</p>
</li>
<li><p>UI-centric thinking – encourages testing everything through the UI</p>
</li>
<li><p>Lack of standardisation – implementations vary widely between teams</p>
</li>
<li><p>Poor scalability - becomes increasingly difficult to manage as application grows</p>
</li>
</ul>
<hr />
<h1 id="heading-flow-model-pattern">Flow Model Pattern</h1>
<h2 id="heading-overview-1">Overview</h2>
<p><a target="_blank" href="https://www.peterfoldhazi.com/flow-model-pattern"><strong>Flow Model Pattern</strong></a> extends <strong>Page Object Model</strong> by adding a workflow abstraction layer between tests and page objects. This layer captures reusable business workflows, reducing code duplication and keeping both page objects and test scripts clean and focused.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765361523508/a3a5af8b-db81-4841-82e3-6cb11555f036.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-description-1">Description</h2>
<p>While developing my latest test automation framework, I found myself caught between two extremes: <strong>POMs</strong> felt too simplistic and led to duplication, while the <strong>Screenplay Pattern</strong> seemed too complex for a deadline-sensitive project where the team lacked experience with advanced patterns.</p>
<p>I needed something that combined the best of both worlds—as simple and straightforward as <strong>Page Object Model</strong>, but with better reusability. The solution was to add a single abstraction layer that would keep <strong>POMs</strong> focused on what they do best (mapping application pages) without the complexity of a full <strong>Screenplay</strong> implementation.</p>
<p>I called this approach <strong><em>User Steps</em></strong>, only to discover later while reading the <a target="_blank" href="https://istqb.org/certifications/certified-tester-advanced-level-test-automation-engineering-ctal-tae-v2-0/"><em>ISTQB Test Automation Engineering Syllabus</em></a> that it already had a name: <strong>Flow Model Pattern</strong>. Turns out I wasn't as original as I thought!</p>
<p>The last thing I want to mention about the <strong>Flow Model Pattern</strong> is that it isn't (very well) standardised, so you have complete flexibility to adapt it to your needs. This makes it particularly well-suited for small to medium teams who want better structure than pure <strong>POM</strong> without the learning curve of <strong>Screenplay Pattern</strong>.</p>
<h2 id="heading-code-example-1">Code Example</h2>
<pre><code class="lang-javascript"><span class="hljs-comment">// For the sake of simplicity and framework-agnosticity</span>
<span class="hljs-comment">// we're skipping the selectors/locators definitions and functions</span>
<span class="hljs-comment">// that are mentioned on the picture, but not used within test case</span>

<span class="hljs-comment">// ===========</span>
<span class="hljs-comment">// TEST SCRIPT</span>
<span class="hljs-comment">// ===========</span>
test(<span class="hljs-string">'should add 20 t-shirts to cart'</span>, <span class="hljs-keyword">async</span> () =&gt; {    
    <span class="hljs-keyword">await</span> ShoppingFlow.addDiverseTShirts(<span class="hljs-number">20</span>);

    <span class="hljs-keyword">const</span> cartCount = <span class="hljs-keyword">await</span> ShoppingFlow.getCartItemCount();
    expect(cartCount).toBe(<span class="hljs-number">20</span>);
});

<span class="hljs-comment">// =============</span>
<span class="hljs-comment">// SHOPPING FLOW</span>
<span class="hljs-comment">// =============</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ShoppingFlow = {
    <span class="hljs-keyword">async</span> addProductToCart(productId) {
        <span class="hljs-keyword">await</span> ProductsPage.openProductDetails(productId);
        <span class="hljs-keyword">await</span> ProductDetailsPage.clickAddButton();
        <span class="hljs-keyword">await</span> ProductDetailsPage.goBackToProductsPage();
    },
    <span class="hljs-keyword">async</span> addMultipleProducts(productIds) {
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> id <span class="hljs-keyword">of</span> productIds) {
            <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.addProductToCart(id);
        }
    },
    <span class="hljs-keyword">async</span> getCartItemCount() {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> ProductDetailsPage.getCartCount();
    },
    <span class="hljs-keyword">async</span> addDiverseTShirts(count) {
        <span class="hljs-keyword">const</span> tshirts = TShirtFactory.createDiverseSet(count);
        <span class="hljs-keyword">const</span> productIds = tshirts.map(<span class="hljs-function"><span class="hljs-params">shirt</span> =&gt;</span> shirt.id);
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.addMultipleProducts(productIds);
    }
}

<span class="hljs-comment">// ================================================================</span>
<span class="hljs-comment">// TEST DATA FACTORY - Clean way to handle the Test Data generation</span>
<span class="hljs-comment">// ================================================================</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> TShirtFactory = {
    create(color, size) {
        <span class="hljs-keyword">const</span> id = <span class="hljs-string">`tshirt-<span class="hljs-subst">${color.toLowerCase()}</span>-<span class="hljs-subst">${size.toLowerCase()}</span>`</span>;
        <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">id</span>: id,
            <span class="hljs-attr">name</span>: <span class="hljs-string">`T-Shirt Model <span class="hljs-subst">${color}</span> <span class="hljs-subst">${size}</span>`</span>,
            <span class="hljs-attr">color</span>: color,
            <span class="hljs-attr">size</span>: size
        };
    },
    createDiverseSet(count) {
        <span class="hljs-keyword">const</span> colors = [<span class="hljs-string">'yellow'</span>, <span class="hljs-string">'black'</span>, <span class="hljs-string">'blue'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'green'</span>, <span class="hljs-string">'turquoise'</span>];
        <span class="hljs-keyword">const</span> sizes = [<span class="hljs-string">'small'</span>, <span class="hljs-string">'medium'</span>, <span class="hljs-string">'large'</span>];

        <span class="hljs-keyword">return</span> <span class="hljs-built_in">Array</span>(count).fill(<span class="hljs-literal">null</span>).map(<span class="hljs-function">(<span class="hljs-params">_, index</span>) =&gt;</span> {
            <span class="hljs-keyword">const</span> color = colors[index % colors.length];
            <span class="hljs-keyword">const</span> size = sizes[index % sizes.length];
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.create(color, size);
        });
    }
};

<span class="hljs-comment">// =============</span>
<span class="hljs-comment">// PRODUCTS PAGE</span>
<span class="hljs-comment">// =============</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ProductsPage = {
    <span class="hljs-keyword">async</span> openProductDetails(id) { ... }
}

<span class="hljs-comment">// ====================</span>
<span class="hljs-comment">// PRODUCT DETAILS PAGE</span>
<span class="hljs-comment">// ====================</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ProductDetailsPage = {
    <span class="hljs-keyword">async</span> clickAddButton() { ... },
    <span class="hljs-keyword">async</span> getCartCount() { ... },
    <span class="hljs-keyword">async</span> goBackToProductsPage() { ... }
}
</code></pre>
<h3 id="heading-pros">✅ PROS</h3>
<ul>
<li><p>Pragmatic middle ground – balances simplicity with scalability</p>
</li>
<li><p>Reduces code duplication – workflows centralised in one place</p>
</li>
<li><p>Keeps Page Objects clean – POMs focus on page interactions only</p>
</li>
<li><p>Easy to learn – intuitive concept, quick onboarding</p>
</li>
<li><p>Improves test readability – tests focus on WHAT, not HOW</p>
</li>
<li><p>Quick to implement – can be added incrementally to existing frameworks</p>
</li>
<li><p>No framework dependency – works with any tool</p>
</li>
<li><p>Scales well for medium projects – handles growth without overwhelming complexity</p>
</li>
</ul>
<h3 id="heading-downsides">❌ DOWNSIDES</h3>
<ul>
<li><p>Not standardised – no official specification or industry consensus</p>
</li>
<li><p>Boundary decisions – requires judgment on what goes in Flows vs Pages</p>
</li>
<li><p>Limited documentation – far fewer resources than POM or Screenplay</p>
</li>
<li><p>Can violate SOLID – risk of becoming a logic "dumping ground"</p>
</li>
<li><p>Less structure – team must establish own conventions</p>
</li>
<li><p>Risk of over-abstraction – can create unnecessary complexity if used too much</p>
</li>
</ul>
<hr />
<h1 id="heading-screenplay-pattern">Screenplay Pattern</h1>
<h2 id="heading-overview-2">Overview</h2>
<p>The <a target="_blank" href="https://serenity-js.org/handbook/design/screenplay-pattern/"><strong>Screenplay Pattern</strong></a> shifts from page-centric (or UI-centric) to actor-centric testing. Tests describe <strong><em>WHO</em></strong> performs <strong><em>WHAT</em></strong> actions to achieve their goals, using <em>abilities</em>, <em>tasks</em>, <em>interactions</em>, and <em>questions</em>. This actor-based approach excels in complex, multi-interface scenarios.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765450364979/5a0a0663-971d-44d3-bad6-a6f37b73e77f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-description-2">Description</h2>
<p>Throughout my career working with all major test automation frameworks, I've observed an interesting pattern: while the <strong>Screenplay Pattern</strong> is well-known in the QA community, practical implementation experience remains relatively scarce. I've personally implemented it once in a production environment, and that experience—combined with extensive research—has given me valuable insights into why adoption remains limited.</p>
<p>This implementation gap isn't unique to my experience. The test automation community frequently grapples with understanding when and how to apply different patterns, as evidenced by ongoing discussions like the Reddit thread <strong><em>"</em></strong><a target="_blank" href="https://www.reddit.com/r/QualityAssurance/comments/1b9we3p/i_dont_get_the_screenplay_pattern/"><em>I don't get the ScreenPlay Pattern</em></a><strong><em>"</em></strong>. What's missing isn't just technical documentation—it's practical guidance on pattern selection based on real-world constraints and team capabilities.</p>
<p>This knowledge gap has significant implications. Many teams default to the <strong>Page Object Model</strong> and extend it with their best judgment—not because it's the <em>optimal fit</em>, but because it's <em>familiar</em>. Meanwhile, projects that could benefit from more sophisticated approaches continue using patterns that ultimately hinder their scalability and maintainability. My goal with this article is to bridge that gap by providing the contextual decision-making framework that's currently absent from most automation discussions—along with clear visual explanations that may finally help you <em>"get the Screenplay Pattern”</em>.</p>
<h3 id="heading-why-screenplay-is-different"><strong>Why Screenplay Is Different</strong></h3>
<p>The <strong>Screenplay Pattern</strong> isn't just “<em>POM with extra layers</em>”—it's a fundamentally different way of thinking about test automation. Instead of organising code around pages and web elements, you organise around actors (users or systems) and their intentions.</p>
<p>Here's what makes it unique:</p>
<ul>
<li><p>Actor-centric thinking: Tests read like user stories ("James logs in and adds a product to cart")</p>
</li>
<li><p>Separation of concerns: <em>Abilities</em>, <em>Tasks</em>, <em>Interactions</em>, and <em>Questions</em> each have single, clear responsibilities</p>
</li>
<li><p>Composability: Small, reusable pieces combine to create complex workflows</p>
</li>
<li><p>Multi-interface support: The same actor can interact with UI, API, and database seamlessly</p>
</li>
<li><p>Scalability: Architecture that handles enterprise complexity without becoming unwieldy</p>
</li>
</ul>
<h3 id="heading-the-trade-off"><strong>The Trade-off</strong></h3>
<p>The price for this sophistication is complexity. Screenplay Pattern requires:</p>
<ul>
<li><p>Strong understanding of object-oriented design principles (especially SOLID)</p>
</li>
<li><p>Longer initial setup and learning curve</p>
</li>
<li><p>Team buy-in and training investment</p>
</li>
<li><p>Commitment to the pattern's structure</p>
</li>
</ul>
<p>For large, long-term projects with experienced teams, these investments pay dividends. For smaller projects or teams without strong programming backgrounds, the pattern can feel like over-engineering. To be completely honest, implementing it in such cases rarely makes sense – the return on investment simply won't justify the effort.</p>
<h2 id="heading-code-example-2">Code Example</h2>
<pre><code class="lang-javascript"><span class="hljs-comment">// For the sake of simplicity and framework-agnosticity</span>
<span class="hljs-comment">// we're skipping the selectors/locators definitions and functions</span>
<span class="hljs-comment">// that are mentioned on the picture, but not used within test case</span>

<span class="hljs-comment">// ===========</span>
<span class="hljs-comment">// TEST SCRIPT</span>
<span class="hljs-comment">// ===========</span>

<span class="hljs-comment">// src/tests/Add20Tshirts.js</span>
test(<span class="hljs-string">'should add 20 t-shirts to cart'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> sarah = Actor.named(<span class="hljs-string">'Sarah'</span>);

    <span class="hljs-keyword">await</span> sarah.attemptsTo(
        AddDiverseTShirtsToCart.count(<span class="hljs-number">20</span>)
    );

    <span class="hljs-keyword">const</span> cartCount = <span class="hljs-keyword">await</span> sarah.asks(CartItemCount.value());
    expect(cartCount).toBe(<span class="hljs-number">20</span>);
});

<span class="hljs-comment">// ==========================================================================</span>
<span class="hljs-comment">// ACTOR – PEOPLE and EXTERNAL SYSTEMS interacting with the system under test</span>
<span class="hljs-comment">// ==========================================================================</span>

<span class="hljs-comment">// src/actor.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Actor = {
    named(name) {
        <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">name</span>: name,
            <span class="hljs-attr">ability</span>: BrowseTheWeb,

            <span class="hljs-keyword">async</span> attemptsTo(...tasks) {
                <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> task <span class="hljs-keyword">of</span> tasks) {
                    <span class="hljs-keyword">await</span> task.performAs(<span class="hljs-built_in">this</span>);
                }
            },

            <span class="hljs-keyword">async</span> asks(question) {
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> question.answeredBy(<span class="hljs-built_in">this</span>);
            }
        };
    }
};

<span class="hljs-comment">// =====================================================================================</span>
<span class="hljs-comment">// ABILITIES – WRAPPERS around any INTEGRATION LIBRARIES (e.g. E2E, API, Cloud Services)</span>
<span class="hljs-comment">// =====================================================================================</span>

<span class="hljs-comment">// src/abilities.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> BrowseTheWeb = {
    <span class="hljs-keyword">async</span> findElement(selector) { <span class="hljs-comment">/* ... */</span> },
    <span class="hljs-keyword">async</span> click(selector) { <span class="hljs-comment">/* ... */</span> },
    <span class="hljs-keyword">async</span> getText(selector) { <span class="hljs-comment">/* ... */</span> }
};

<span class="hljs-comment">// ==========================================================================</span>
<span class="hljs-comment">// TASKS - SEQUENCES OF ACTIVITIES as meaningful steps of a business workflow</span>
<span class="hljs-comment">// ==========================================================================</span>

<span class="hljs-comment">// src/tasks/AddDiverseTShirtsToCart.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> AddDiverseTShirtsToCart = {
    count(numberOfShirts) {
        <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">numberOfShirts</span>: numberOfShirts,

            <span class="hljs-keyword">async</span> performAs(actor) {
                <span class="hljs-keyword">const</span> tshirts = TShirtFactory.createDiverseSet(<span class="hljs-built_in">this</span>.numberOfShirts);

                <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> tshirt <span class="hljs-keyword">of</span> tshirts) {
                    <span class="hljs-keyword">await</span> actor.attemptsTo(
                        AddProductToCart.withId(tshirt.id)
                    );
                }
            }
        };
    }
};

<span class="hljs-comment">// src/tasks/AddProductToCart.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> AddProductToCart = {
    withId(productId) {
        <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">productId</span>: productId,

            <span class="hljs-keyword">async</span> performAs(actor) {
                <span class="hljs-keyword">await</span> actor.attemptsTo(
                    OpenProductDetails.for(<span class="hljs-built_in">this</span>.productId),
                    Click.on(<span class="hljs-string">'.add-button'</span>),
                    Click.on(<span class="hljs-string">'.back-to-products'</span>)
                );
            }
        };
    }
};

<span class="hljs-comment">// ================================================================================</span>
<span class="hljs-comment">// INTERACTIONS - LOW-LEVEL ACTIVITIES an ACTOR can perform using a given interface</span>
<span class="hljs-comment">// ================================================================================</span>

<span class="hljs-comment">// src/interactions/OpenProductDetails.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> OpenProductDetails = {
    <span class="hljs-keyword">for</span>(productId) {
        <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">productId</span>: productId,

            <span class="hljs-keyword">async</span> performAs(actor) {
                <span class="hljs-keyword">const</span> selector = <span class="hljs-string">`[data-product-id="<span class="hljs-subst">${<span class="hljs-built_in">this</span>.productId}</span>"]`</span>;
                <span class="hljs-keyword">await</span> actor.attemptsTo(
                    Click.on(selector)
                );
            }
        };
    }
};

<span class="hljs-comment">// src/interactions/Click.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Click = {
    on(selector) {
        <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">selector</span>: selector,

            <span class="hljs-keyword">async</span> performAs(actor) {
                <span class="hljs-keyword">await</span> actor.ability.click(<span class="hljs-built_in">this</span>.selector);
            }
        };
    }
};

<span class="hljs-comment">// ====================================================================================</span>
<span class="hljs-comment">// QUESTIONS - RETRIEVE INFORMATION from the system under test and the test environment</span>
<span class="hljs-comment">// ====================================================================================</span>

<span class="hljs-comment">// src/questions/CartItemCount.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> CartItemCount = {
    value() {
        <span class="hljs-keyword">return</span> {
            <span class="hljs-keyword">async</span> answeredBy(actor) {
                <span class="hljs-keyword">const</span> cartBadge = <span class="hljs-keyword">await</span> actor.ability.findElement(<span class="hljs-string">'.cart-badge'</span>);
                <span class="hljs-keyword">const</span> text = <span class="hljs-keyword">await</span> actor.ability.getText(<span class="hljs-string">'.cart-badge'</span>);
                <span class="hljs-keyword">return</span> <span class="hljs-built_in">parseInt</span>(text, <span class="hljs-number">10</span>);
            }
        };
    }
};

<span class="hljs-comment">// ================================================================</span>
<span class="hljs-comment">// TEST DATA FACTORY - Clean way to handle the Test Data generation</span>
<span class="hljs-comment">// ================================================================</span>

<span class="hljs-comment">// src/utils/TShirtFactory.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> TShirtFactory = {
    create(color, size) {
        <span class="hljs-keyword">const</span> id = <span class="hljs-string">`tshirt-<span class="hljs-subst">${color.toLowerCase()}</span>-<span class="hljs-subst">${size.toLowerCase()}</span>`</span>;
        <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">id</span>: id,
            <span class="hljs-attr">name</span>: <span class="hljs-string">`T-Shirt Model <span class="hljs-subst">${color}</span> <span class="hljs-subst">${size}</span>`</span>,
            <span class="hljs-attr">color</span>: color,
            <span class="hljs-attr">size</span>: size
        };
    },

    createDiverseSet(count) {
        <span class="hljs-keyword">const</span> colors = [<span class="hljs-string">'yellow'</span>, <span class="hljs-string">'black'</span>, <span class="hljs-string">'blue'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'green'</span>, <span class="hljs-string">'turquoise'</span>];
        <span class="hljs-keyword">const</span> sizes = [<span class="hljs-string">'small'</span>, <span class="hljs-string">'medium'</span>, <span class="hljs-string">'large'</span>];

        <span class="hljs-keyword">return</span> <span class="hljs-built_in">Array</span>(count).fill(<span class="hljs-literal">null</span>).map(<span class="hljs-function">(<span class="hljs-params">_, index</span>) =&gt;</span> {
            <span class="hljs-keyword">const</span> color = colors[index % colors.length];
            <span class="hljs-keyword">const</span> size = sizes[index % sizes.length];
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.create(color, size);
        });
    }
};
</code></pre>
<h2 id="heading-summary-1">Summary</h2>
<h3 id="heading-pros-1">✅ PROS</h3>
<ul>
<li><p>Highly maintainable</p>
</li>
<li><p>Multi-interface support – seamlessly combines UI, API, and database interactions</p>
</li>
<li><p>SOLID design principles – follows Single Responsibility, Open-Closed principles</p>
</li>
<li><p>Business-focused language – tests read like user stories/workflows</p>
</li>
<li><p>Better abstraction layers – Tasks, Actions, Questions provide clear organisation</p>
</li>
<li><p>Excellent scalability – handles complex, multi-actor scenarios well</p>
</li>
<li><p>Reusable components – interactions and tasks can be composed and reused</p>
</li>
</ul>
<h3 id="heading-downsides-1">❌ DOWNSIDES</h3>
<ul>
<li><p>Steeper learning curve – much more complex concepts to understand initially</p>
</li>
<li><p>Limited community knowledge – fewer practitioners with hands-on experience</p>
</li>
<li><p>Requires stronger technical skills – team needs understanding of design patterns</p>
</li>
<li><p>Overkill for simple projects – unnecessary overhead for small and even medium-sized applications</p>
</li>
<li><p>Framework dependency – typically requires specific frameworks (e.g. Serenity/JS, Boa Constrictor, ScreenPy)</p>
</li>
<li><p>Longer onboarding time – new team members take longer to become productive</p>
</li>
</ul>
<hr />
<h1 id="heading-choosing-the-right-approach">Choosing the Right Approach</h1>
<p>Here are my recommendations for different project settings. As a general rule of thumb: stick with the <strong>Keep It Stupidly Simple</strong> (KISS) paradigm and don't over-engineer in the early stages.</p>
<p>When it comes to different project contexts, here are my suggestions:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I use the umbrella term <strong><em>Test Maintainers</em></strong> because the test framework can be maintained by QA engineers, technical architects and software developers alike.</div>
</div>

<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Project Type</strong></td><td><strong>Test Maintainers Count</strong></td><td><strong>Recommended Strategy</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Small and Simple</td><td>Small (1-3)</td><td><strong>Page Object Model</strong> (extend to <strong>Flow Model</strong> if code maintenance becomes unmanageable).</td></tr>
<tr>
<td>Medium and Complex</td><td>Small (1-3)</td><td><strong>Page Object Model</strong> with an idea of extension to <strong>Flow Model</strong> once you have identified the areas that would benefit most from it.</td></tr>
<tr>
<td>Large and Complex</td><td>Small (1-3)</td><td><strong>Page Object Model</strong> with an idea of extension to <strong>Flow Model</strong> once you have identified the areas that would benefit most from it. If the team grows and you have enough technical knowledge - gradually introduce the <strong>Screenplay Pattern</strong> alongside your existing framework.</td></tr>
<tr>
<td>Large and Complex</td><td>Large (more than 4)</td><td>The <strong>Screenplay Pattern</strong> is ideal for larger teams as it is highly effective in complex environments and ensures greater compliance with programming best practices.</td></tr>
</tbody>
</table>
</div><p>If you have any questions, feel free to leave a comment.</p>
<p>Thanks and happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Using WebdriverIO v9 for Effective Cross-Platform End-to-End Testing]]></title><description><![CDATA[Preface
In a previous article from the start of this year, I mentioned various frameworks that can be used for mobile test automation. The one that came up repeatedly was WebdriverIO, which I have been using actively for over two years now.
It is a s...]]></description><link>https://engineering.cloudflight.io/using-webdriverio-v9-for-effective-cross-platform-end-to-end-testing</link><guid isPermaLink="true">https://engineering.cloudflight.io/using-webdriverio-v9-for-effective-cross-platform-end-to-end-testing</guid><category><![CDATA[Webdriver.io]]></category><category><![CDATA[end to end testing]]></category><dc:creator><![CDATA[Tomasz Buga]]></dc:creator><pubDate>Mon, 17 Nov 2025 08:24:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/G85NA5FuQCo/upload/7ba3cc174bbcc05f97682e0f897c70e7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-preface">Preface</h2>
<p>In a <a target="_blank" href="https://hashnode.com/post/cm5nm417h000i09la1o9x0uyy">previous article from the start of this year</a>, I mentioned various frameworks that can be used for mobile test automation. The one that came up repeatedly was <strong>WebdriverIO</strong>, which I have been using actively for over two years now.</p>
<p>It is a shame that there are no really comprehensive articles on how to use <strong>WebdriverIO</strong>, especially when it comes to automating complex scenarios such as cross-platform testing of <strong>iOS</strong>, <strong>Android</strong> and <strong>Web</strong> at the same time.</p>
<p>Thanks to <strong>Cloudflight's</strong> courtesy, I was able to open-source the <strong>WebdriverIO</strong> sandbox that I created for internal use and knowledge sharing. I will use this as the basis for this article.</p>
<p><a target="_blank" href="https://github.com/cloudflightio/cross-platform-test-framework">https://github.com/cloudflightio/cross-platform-test-framework</a></p>
<p>The demo setup of the sandbox framework is based on the well-known <strong>Wikipedia</strong> application.</p>
<hr />
<h1 id="heading-getting-started">Getting Started</h1>
<h2 id="heading-want-to-try-out-the-framework">Want to try out the framework?</h2>
<p>The complete setup guide with all prerequisites, installation steps, and your first test run is available in the <a target="_blank" href="https://github.com/cloudflightio/cross-platform-test-framework">project READM</a><a target="_blank" href="https://github.com/cloudflightio/cross-platform-test-framework">E.</a></p>
<h2 id="heading-quick-start-for-the-impatient">Quick start for the impatient</h2>
<pre><code class="lang-bash">yarn install
yarn run wdio:web:edge
</code></pre>
<p>The framework supports Web (Edge), Android, and iOS testing out of the box. Detailed platform-specific setup instructions are in the <a target="_blank" href="https://github.com/cloudflightio/cross-platform-test-framework">READM</a><a target="_blank" href="https://github.com/cloudflightio/cross-platform-test-framework">E</a>.</p>
<hr />
<h1 id="heading-deep-dive">Deep dive</h1>
<h2 id="heading-why-yet-another-boilerplate-project">Why yet another Boilerplate Project?</h2>
<p>Before we take a closer look at the features of our sandbox, I would like to briefly explain why I felt the need to create it in the first place.</p>
<p>When you start working with the <strong>WebdriverIO</strong>, the <a target="_blank" href="https://webdriver.io/docs/boilerplates">Boilerplate Projects</a> page is one of the first places you’ll visit. Although there are a plethora of different setups, I couldn’t find anything that suited my personal needs.</p>
<p>You might be wondering what those needs were. Here are the features that I couldn't find in any of the available boilerplate projects:</p>
<ol>
<li><p><strong>Standardised, simple selectors handling</strong> — consistent patterns that work across all platforms.</p>
</li>
<li><p><strong>The Page Object Model pattern</strong> — an industry standard with which every test automation engineer should be familiar. It's effective because it's simple and intuitive.</p>
</li>
<li><p><strong>Simple, maintainable architecture</strong> — avoiding over-engineering and façade frameworks (like Cucumber) that add disproportionate complexity without delivering equivalent practical value. In my 6+ years of test automation experience, over-engineered frameworks consistently become maintenance burdens when the original developers leave, often requiring teams to reverse engineer their own codebase.</p>
</li>
<li><p><strong>Write-once-run-everywhere test spec files</strong> — we'll explore this concept in detail later in this article.</p>
</li>
<li><p><strong>Simple reporting integrated directly into the boilerplate</strong> — no additional setup required.</p>
</li>
<li><p><strong>A true cross-platform example implementation</strong> covering all major platforms (Web, Android, and iOS) that works out-of-the-box.</p>
</li>
</ol>
<p>Now that we've outlined these pain points, let's examine the most important ones and see how our framework solves them.</p>
<hr />
<h1 id="heading-selectors-handling">Selectors Handling</h1>
<h2 id="heading-the-select-function-overview">The <code>select()</code> Function Overview</h2>
<p>The word <em>simple</em> that I'm using here might seem subjective at first glance. Especially, if you take a look at the implementation of the code, where we have the following:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// test/common/sharedCommands.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">select</span>(<span class="hljs-params">selector: Selector</span>): <span class="hljs-title">ChainablePromiseElement</span> </span>{}
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">selectArray</span>(<span class="hljs-params">selector: SelectorArray</span>): <span class="hljs-title">ChainablePromiseArray</span> </span>{}

<span class="hljs-comment">// test/common/selectors.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> selectors = {
    homePage: {
        searchField: {
            android: <span class="hljs-string">'org.wikipedia:id/search_container'</span>,
            ios: <span class="hljs-string">'**/XCUIElementTypeSearchField[`label == "Search Wikipedia"`]'</span>,
            web: <span class="hljs-string">'(//input[@name="search"])[1]'</span>,
            mobileBrowser: <span class="hljs-string">'//form[@id="minerva-overlay-search"]//input[@name="search"]'</span>,
        },
    }
}

<span class="hljs-comment">// test/pageobjects/home.page.ts</span>
searchField: <span class="hljs-function">() =&gt;</span> select({
    ...selectors.homePage.searchField,
    iosSelectionMethod: getByClassChain,
}),
</code></pre>
<p>It looks anything but <em>simple</em>.</p>
<p>However, when you consider the alternatives—<em>how would you approach this differently?</em>—you can see why this solution is actually elegant and robust.</p>
<p>To illustrate this, here's how you might approach cross-platform selectors in a naive way:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// *************</span>
<span class="hljs-comment">// THE NAIVE WAY</span>
<span class="hljs-comment">// *************</span>

<span class="hljs-comment">// test/pageobjects/home.page.ts - example without selectors handling</span>
searchField: <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (browser.isNativeContext) {
        <span class="hljs-keyword">return</span> browser.isAndroid ?
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'(//android.widget.TextView[@text="Appium"])[1]'</span>) :
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'-ios class chain:**/XCUIElementTypeStaticText[`label == "Appium"`][2]'</span>);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> browser.isMobile ?
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'//input[@name="search"])[1]'</span>) :
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'//form[@id="minerva-overlay-search"]//input[@name="search"]'</span>);
    }
}

<span class="hljs-comment">// ********************</span>
<span class="hljs-comment">// THE STANDARDISED WAY</span>
<span class="hljs-comment">// ********************</span>

<span class="hljs-comment">// test/pageobjects/home.page.ts</span>
searchField: <span class="hljs-function">() =&gt;</span> select({
    ...selectors.homePage.searchField,
    iosSelectionMethod: getByClassChain,
}),

<span class="hljs-comment">// test/common/selectors.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> selectors = {
    homePage: {
        searchField: {
            android: <span class="hljs-string">'org.wikipedia:id/search_container'</span>,
            ios: <span class="hljs-string">'**/XCUIElementTypeSearchField[`label == "Search Wikipedia"`]'</span>,
            web: <span class="hljs-string">'(//input[@name="search"])[1]'</span>,
            mobileBrowser: <span class="hljs-string">'//form[@id="minerva-overlay-search"]//input[@name="search"]'</span>,
        },
    }
}
</code></pre>
<p>With just one selector, this might not seem like much of an improvement, but add a few more selectors and the naive approach becomes ridiculous compared to our standardised way:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// *************</span>
<span class="hljs-comment">// THE NAIVE WAY</span>
<span class="hljs-comment">// *************</span>

<span class="hljs-comment">// test/pageobjects/home.page.ts - example without selectors handling</span>
searchField: <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (browser.isNativeContext) {
        <span class="hljs-keyword">return</span> browser.isAndroid ?
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'org.wikipedia:id/search_container'</span>) :
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'-ios class chain:**/XCUIElementTypeSearchField[`label == "Search Wikipedia"`]`][2]'</span>);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> browser.isMobile ?
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'//input[@name="search"])[1]'</span>) :
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'//form[@id="minerva-overlay-search"]//input[@name="search"]'</span>);
    }
},
searchResultItem: <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (browser.isNativeContext) {
        <span class="hljs-keyword">return</span> browser.isAndroid ?
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'id:org.wikipedia:id/page_list_item_title'</span>) :
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'-ios predicate string:label == "Appium"'</span>);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> browser.isMobile ?
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'//li[@title="Appium"]'</span>) :
            <span class="hljs-keyword">await</span> browser.$(<span class="hljs-string">'//li[@title="Appium"]//a'</span>);
    }
}

<span class="hljs-comment">// ********************</span>
<span class="hljs-comment">// THE STANDARDISED WAY</span>
<span class="hljs-comment">// ********************</span>

<span class="hljs-comment">// test/pageobjects/home.page.ts - an actual excerpt from the codebase</span>
searchField: <span class="hljs-function">() =&gt;</span> select({
    ...selectors.homePage.searchField,
    androidSelectionMethod: getById,
    iosSelectionMethod: getByClassChain,
}),
searchResultItem: <span class="hljs-function">() =&gt;</span> select({
    ...selectors.homePage.searchResultItem,
    androidSelectionMethod: getById,
    iosSelectionMethod: getByPredicateString,
}),
</code></pre>
<p>And don't even get me started on handling the Android and iOS selection methods—just look at the <code>id:</code>, <code>-ios class chain:</code>, and <code>-ios predicate string:</code> prefixes in the naive approach.</p>
<h2 id="heading-the-selectarray-function-overview">The <code>selectArray()</code> Function Overview</h2>
<p>Since we thoroughly covered <code>select()</code>, I don't think we need to delve deeply into <code>selectArray()</code> since it functions similarly. However, it's important to explain why we need it.</p>
<p>The best time to use the <code>selectArray()</code> function is generally when you need to count the number of elements.</p>
<p>Some might argue that it's useful for extracting an array of <em>WebElements</em>. That's a fair point, but I strongly recommend getting familiar with using variables in selectors (covered in the next section). For me, the <code>select()</code> function combined with variables is sufficient 99% of the time.</p>
<p>One more thing: <code>select()</code> and <code>selectArray()</code> are simply facades over regular <code>$</code> and <code>$$</code> WebdriverIO’s commands, and since they return the <code>ChainablePromiseElement</code> and <code>ChainablePromiseArray</code>, you can refer to the official WebdriverIO documentation for more information on how to use them in more complex cases:</p>
<ol>
<li><p><a target="_blank" href="https://webdriver.io/docs/selectors">https://webdriver.io/docs/selectors</a></p>
</li>
<li><p><a target="_blank" href="https://webdriver.io/docs/api/browser/$">https://webdriver.io/docs/api/browser/$</a></p>
</li>
<li><p><a target="_blank" href="https://webdriver.io/docs/api/browser/$$">https://webdriver.io/docs/api/browser/$$</a></p>
</li>
</ol>
<h2 id="heading-selectors-with-variables">Selectors with Variables</h2>
<p>Sometimes, we want to use selectors with variables. The most common scenario would be an attribute that has a dynamically generated counter (e.g., <code>item-0-dropdown</code>, <code>item-1-dropdown</code> etc.).</p>
<h3 id="heading-without-variables">Without Variables</h3>
<p>Let's start again with the naive, straightforward approach—we might end up with a hardcoded selector like this.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>Note: For the sake of simplicity, I'll use the standardised approach with the </em><code>selectors.ts</code><em> file combined with the </em><code>select()</code><em> function for selector management.</em></div>
</div>

<pre><code class="lang-typescript"><span class="hljs-comment">// *************</span>
<span class="hljs-comment">// THE NAIVE WAY</span>
<span class="hljs-comment">// *************</span>

<span class="hljs-comment">// test/common/selectors.ts - example without variables</span>
homePage: {
    firstDropdownItem: {
        ios: <span class="hljs-string">'//XCUIElementTypeStaticText[@name, "item-0-dropdown"]'</span>,
        android: <span class="hljs-string">'//android.widget.TextView[@text, "item-0-dropdown"]'</span>,
        web: <span class="hljs-string">'//*[@data-testid="item-0-dropdown"]'</span>,
        mobileBrowser: <span class="hljs-string">'//*[@data-testid="item-0-dropdown"]//span'</span>,
    },
    secondDropdownItem: {
        ios: <span class="hljs-string">'//XCUIElementTypeStaticText[@name, "item-1-dropdown"]'</span>,
        android: <span class="hljs-string">'//android.widget.TextView[@text, "item-1-dropdown"]'</span>,
        web: <span class="hljs-string">'//*[@data-testid="item-1-dropdown"]'</span>,
        mobileBrowser: <span class="hljs-string">'//*[@data-testid="item-1-dropdown"]//span'</span>,
    },
    thirdDropdownItem: {
        ios: <span class="hljs-string">'//XCUIElementTypeStaticText[@name, "item-2-dropdown"]'</span>,
        android: <span class="hljs-string">'//android.widget.TextView[@text, "item-2-dropdown"]'</span>,
        web: <span class="hljs-string">'//*[@data-testid="item-2-dropdown"]'</span>,
        mobileBrowser: <span class="hljs-string">'//*[@data-testid="item-2-dropdown"]//span'</span>,
    }
}

<span class="hljs-comment">// test/pageobjects/home.page.ts - example without variables</span>
firstDropdownItem: <span class="hljs-function">() =&gt;</span> select(selectors.homePage.firstDropdownItem)
secondDropdownItem: <span class="hljs-function">() =&gt;</span> select(selectors.homePage.secondDropdownItem)
thirdDropdownItem: <span class="hljs-function">() =&gt;</span> select(selectors.homePage.thirdDropdownItem)

<span class="hljs-comment">// test/pageobjects/home.page.ts - example code usage</span>
<span class="hljs-keyword">async</span> selectDropdownItemByIndex(itemIndex: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
  <span class="hljs-keyword">switch</span> (itemIndex) {
    <span class="hljs-keyword">case</span> <span class="hljs-number">0</span>:
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.firstDropdownItem().click();
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-number">1</span>:
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.secondDropdownItem().click();
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-number">2</span>:
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.thirdDropdownItem().click();
      <span class="hljs-keyword">break</span>;
  <span class="hljs-keyword">default</span>:
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Invalid dropdown item index: <span class="hljs-subst">${itemIndex}</span>.`</span>);
  }
},
</code></pre>
<h4 id="heading-with-variables"><strong>With Variables</strong></h4>
<p>The second method allows us to provide dynamic data, and make our tests more robust.</p>
<p>To use variables, create a selector providing the variable in curly braces <code>{{itemIndex}}</code>, then pass the variable to the <code>select()</code> or <code>selectArray()</code> function within your <em>Page Object Model</em> file.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>Note: If the </em><code>mobileBrowser</code><em> selector is the same as the </em><code>web</code><em> selector you can use just the </em><code>web</code><em> one, as it will propagate the value to </em><code>mobileBrowser</code><em> as well.</em></div>
</div>

<pre><code class="lang-typescript"><span class="hljs-comment">// ********************</span>
<span class="hljs-comment">// THE STANDARDISED WAY</span>
<span class="hljs-comment">// ********************</span>

<span class="hljs-comment">// test/common/selectors.ts - example with variables</span>
homePage: {
    dropdownItem: {
        ios: <span class="hljs-string">'//XCUIElementTypeStaticText[@name, "item-{{itemIndex}}-dropdown"]'</span>,
        android: <span class="hljs-string">'//android.widget.TextView[@text, "item-{{itemIndex}}-dropdown"]'</span>, 
        web: <span class="hljs-string">'//*[@data-testid="item-{{itemIndex}}-dropdown"]'</span>
    }
}

<span class="hljs-comment">// test/pageobjects/home.page.ts - example with variables</span>
nthDropdownItem: <span class="hljs-function">(<span class="hljs-params">dropdownItemIndex: <span class="hljs-built_in">number</span></span>) =&gt;</span>
    select({
        ...selectors.homePage.dropdownItem,
        variables: { itemIndex: <span class="hljs-string">`<span class="hljs-subst">${dropdownItemIndex}</span>`</span> },
    }),

<span class="hljs-comment">// test/pageobjects/home.page.ts - example code usage</span>
<span class="hljs-keyword">async</span> selectDropdownItemByIndex(itemIndex: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
  <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.nthDropdownItem(itemIndex).click();
},
</code></pre>
<hr />
<h1 id="heading-simple-amp-maintainable-architecture"><strong>Simple &amp; Maintainable Architecture</strong></h1>
<h2 id="heading-end-to-end-tests-are-not-regular-applications-period">End-to-end tests are not regular applications, period.</h2>
<p>While <em>abstractions</em>, <em>inheritance, SOLID, DRY,</em> and <em>design patterns</em> have their place in test automation, you must adjust your mindset. In most cases, you'll be working with less experienced developers or testers fresh from programming courses who lack battle-tested experience. Over-engineering your test framework with advanced patterns may seem elegant, but it often creates a maintenance nightmare for the very people who need to work with it daily.</p>
<blockquote>
<p><strong><em>If you reach the point where you feel your test scripts need their own tests to keep everything in place—you're doing something wrong.</em></strong></p>
</blockquote>
<p>Overly complex frameworks only work when you have senior test automation engineers on the team—but from an economic perspective, most companies can't justify hiring multiple senior-level testers. Moreover, since senior test automation engineers possess skills comparable to senior software developers, they often transition into software development roles for better career opportunities unless they have a genuine passion for testing.</p>
<h2 id="heading-the-solution-focus-on-the-kiss-principle"><strong>The solution? Focus on the KISS principle</strong></h2>
<p>Keep It Stupidly Simple (KISS)—write code that anyone can work with.</p>
<p>The ideal scenario: your test scripts should be simple enough that new tests can be created through straightforward copy-paste with minimal modifications.</p>
<p>Build from the bottom-up: write the simplest code that works first, then refactor. That's why our framework uses straightforward building blocks:</p>
<ul>
<li><p><strong>Page Object Models</strong> — encapsulate page interactions and behaviors</p>
</li>
<li><p><strong>Centralized Selectors</strong> — single source of truth for element identification</p>
</li>
<li><p><strong>Test Scripts</strong> — contain your test logic and assertions</p>
</li>
<li><p><strong>Configuration Files</strong> — pre-configured settings that let you start testing immediately</p>
</li>
<li><p><strong>Shared Commands</strong> — utility functions you can freely modify depending on your needs, avoiding dependencies on inflexible third-party libraries that require forking to extend</p>
</li>
<li><p><strong>Custom Matchers</strong> — extend default WebdriverIO assertions with custom message logging for better reporting and easier debugging</p>
</li>
<li><p><strong>Flows</strong> (optional, for larger frameworks) — reusable test sequences (<a target="_blank" href="https://www.peterfoldhazi.com/flow-model-pattern">learn more</a>)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761751080295/2104555a-41aa-481b-a548-03a1e258dc8d.png" alt class="image--center mx-auto" /></p>
<hr />
<h1 id="heading-write-once-run-everywhere-test-spec-files"><strong>Write-Once-Run-Everywhere Test Spec Files</strong></h1>
<p>When I first encountered cross-platform automation, I was completely clueless about how it should work. But as I started writing my first test cases, I quickly realized there was potential for standardization.</p>
<p>If your application looks and behaves the same across all platforms, it's super easy to automate using our framework. The main effort is extracting selectors for each platform (DevTools for web, Appium Inspector for mobile). If your application behaves slightly differently but shares most functionality, simple conditional statements handle the variations.</p>
<p>The best part? You can reuse the same test code across all platforms, thanks to WebdriverIO's extreme versatility.</p>
<p>Below is an excerpt from our Wikipedia test example, demonstrating how platform-specific variations are handled while keeping the script readable and contained in a single file:</p>
<pre><code class="lang-typescript">describe(<span class="hljs-string">`Wikipedia`</span>, <span class="hljs-function">() =&gt;</span> {
  it(<span class="hljs-string">'Search for an article about "Test"'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> addTestId(<span class="hljs-string">'TEST-1'</span>);

    <span class="hljs-keyword">if</span> (!browser.isNativeContext) {
      <span class="hljs-keyword">await</span> homePage.openUrl();
    }

    <span class="hljs-keyword">if</span> (browser.isNativeContext) {
      <span class="hljs-keyword">await</span> homePage.pressSkipButton();
    }

    <span class="hljs-keyword">await</span> homePage.enterTextToSearchField(<span class="hljs-string">'Test'</span>);
    <span class="hljs-keyword">await</span> homePage.pressFirstSearchResultItem();
    <span class="hljs-keyword">await</span> wikipediaGamesModal.closeModal();
    <span class="hljs-keyword">await</span> articlePage.waitForPageLoad();
    <span class="hljs-keyword">const</span> pageTitle = <span class="hljs-keyword">await</span> articlePage.getPageTitle();

    <span class="hljs-keyword">await</span> expect(pageTitle).toEqualString(<span class="hljs-string">'Test'</span>);

    <span class="hljs-keyword">await</span> takeScreenshotWithTitle(<span class="hljs-string">'Successful test - Page title with the "Test" value'</span>);
  })
})
</code></pre>
<hr />
<h1 id="heading-simple-reporting-integrated-directly-into-the-boilerplate"><strong>Simple Reporting Integrated Directly into the Boilerplate</strong></h1>
<p>I don't know about you, but I really dislike having too many choices when setting up a new technology. I want something decided for me, with the flexibility to change it if needed.</p>
<p>The reason? A shorter ramp-up period. When you have a clear picture of what's what, you can learn it quickly and then fine-tune. Simply put—I just want to drive a car, not assemble it from a DIY kit.</p>
<p>That's why our framework has reporting baked in out of the box, with three layers:</p>
<ol>
<li><p><strong>Basic logs</strong> — WebdriverIO's standard console output</p>
</li>
<li><p><strong>Extended log steps</strong> — Custom logs embedded in shared commands that output test steps descriptions to both the console and Allure reports</p>
</li>
<li><p><strong>Allure Report</strong> — Post-test HTML report that can be launched locally with <code>allure serve</code> or hosted (e.g., on GitHub Pages)</p>
</li>
</ol>
<hr />
<h1 id="heading-wrapping-up">Wrapping Up</h1>
<p>If you made it this far, thank you for your time. I hope you now have a clearer understanding of what our Test Automation Framework can do for you and how it can speed up your WebdriverIO-based cross-platform end-to-end setup.</p>
<p>If you have any questions, feel free to leave a comment. Contributions are welcome—feel free to open pull requests. While we don't have a formal code of conduct for contributors yet, that's something I plan to add in the future.</p>
<p>Thanks and happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[UX Writing for Business Applications]]></title><description><![CDATA[Let’s be honest - who wakes up and and is excited to read button labels or error messages? But have you ever clicked a vague “Next” button and immediately regretted it because you had no idea where it was taking you? Or stared at an error message lik...]]></description><link>https://engineering.cloudflight.io/ux-writing-for-business-applications</link><guid isPermaLink="true">https://engineering.cloudflight.io/ux-writing-for-business-applications</guid><category><![CDATA[UX]]></category><category><![CDATA[ux writing]]></category><category><![CDATA[Business Applications]]></category><category><![CDATA[b2b]]></category><dc:creator><![CDATA[Sonja Frisch]]></dc:creator><pubDate>Wed, 08 Oct 2025 14:07:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/v9FQR4tbIq8/upload/e7438d6cc4dfcb2cd7dfe36d58ba8c41.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let’s be honest - who wakes up and and is excited to read button labels or error messages? But have you ever clicked a vague “Next” button and immediately regretted it because you had no idea where it was taking you? Or stared at an error message like “Something went wrong” and thought, <em>Great. That’s helpful</em>.</p>
<p>Now, imagine this is happening in your company’s payroll and budgeting software. A single misleading label or poorly worded error message can have serious consequences, from lost data to financial impacts. UX writing in B2B isn’t just about making things sound nice - it’s about making sure users can do their jobs. It’s the difference between “No data found” and “No entries for this period. Try adjusting your filters.” It’s also about knowing when to keep it formal, when to add a touch of personality, and when to just stay out of the way.</p>
<p>So, let’s dive into the world of UX writing for B2B SaaS - the place where clarity meets complexity, tooltips become lifesavers, and error messages don’t make you want to throw your laptop out the window.</p>
<h3 id="heading-so-what-is-ux-writing">So, what is UX writing?</h3>
<p>UX writing is the art of writing clear, concise and functional text that guides users through digital products. It’s the microcopy you see on buttons, error messages, empty states, onboarding instructions and tooltips—everywhere text helps users take action.</p>
<p>While the fundamentals of usability remain the same, the design approach in enterprise products tends to differ from consumer. Enterprise products are more often than not complex workflow tools that involve organizing and displaying large amounts of data. This makes strong, well-structured design essential to support users getting their jobs done.</p>
<h3 id="heading-keep-it-simple-not-always">Keep it simple? Not always</h3>
<p>Simplicity is a core design principle, but in B2B software oversimplifying a feature through words can undermine its meaning or the consequence of an action. Workflows in enterprises are complex, and the tools must work in that complexity. What does this mean in terms of UX writing?</p>
<p><strong>Use terms and language your audience understands</strong></p>
<p>Enterprise users are people with specific industry expertise, and they expect to see the terms and phrases relevant to their work. Removing or oversimplifying those terms in an attempt to "keep it simple" can create unnecessary friction and even undermine a feature’s meaning or consequence of an action. UX writers must familiarize themselves with the language their users already understand. One of the best ways to do this is by collecting domain knowledge from users, stakeholders, product managers and other sources like internal training documents or customer support tickets.</p>
<p><strong>Abbreviations matter</strong></p>
<p>Abbreviations are another challenge in B2B UX writing. While they are often necessary in technical or industry-specific contexts, they must be used consistently and, when possible, explained in a way that supports both new and experienced users. For example, an abbreviation might be well known within a particular industry but unfamiliar to a new hire or someone switching from a competitor’s product. Striking the right balance between precision and comprehensibility is key.</p>
<h3 id="heading-product-guidance-hello-tooltips">Product guidance: Hello tooltips!</h3>
<p>B2B SaaS products often come with a learning curve, especially when they involve complex user flows and multiple user roles. Most onboarding information is only needed as a one-time learning and well-placed tooltips can provide just-in-time guidance without overwhelming the user.</p>
<p>These micro-interactions should be brief and helpful, offering just enough information to guide the user to the next step. A tooltip that appears when hovering over a setting, for example, can explain what the option does without requiring the user to leave their workflow to search for documentation.</p>
<h3 id="heading-delight-with-caution-striking-the-right-balance-in-enterprise-ux">Delight with caution: Striking the Right Balance in Enterprise UX</h3>
<p>While consumer products often add delight through playful copy and animations, this approach doesn’t always translate well to enterprise software. Users handle complex tasks and delight can become a distraction. Many users are already experts in the tool and rely on it daily, often repeating the same workflows. Flashy animations or quirky copy can quickly feel annoying and disruptive.</p>
<p>This doesn’t mean there’s no room for delight—it just needs to be subtle and well-timed. By being very thoughtful and finding innovative ways to add value, you can create micro-experiences that make someone smile or inspire them.</p>
<p>Here’s a couple of ideas that can make a product feel more engaging:</p>
<ul>
<li><p>Language: Use positive, active language and balance your brand voice with the language users actually speak.</p>
</li>
<li><p>Empty states: Have smart empty states or defaults, teach users something they didn’t know.</p>
</li>
<li><p>Success Messages: Find small moments of celebrations to share.</p>
</li>
</ul>
<h3 id="heading-final-thoughts"><strong>Final thoughts</strong></h3>
<p>UX writing in B2B is about more than just making things sound good—it’s about clarity, precision, and helping users get their work done. The goal is to use the right language for the right audience. Whether it's through well-placed tooltips, clear error messages, or actionable labels, good UX writing reduces friction, improves efficiency, and ultimately leads to higher product adoption and satisfaction.</p>
<p>For teams working on B2B products, now is the time to take a closer look at the microcopy within your application. Are labels explicit? Are tooltips helpful? Are error messages guiding users rather than frustrating them? Small changes in UX writing can have a big impact, making the difference between a product that feels intuitive and one that users struggle with.</p>
]]></content:encoded></item><item><title><![CDATA[Open LLMs, Real Results: A series on Free-to-Use AI Models]]></title><description><![CDATA[Part 1 - An Introduction to the matter
What will this series be about?
During this series of blog posts, I will do my best to provide my opinion on specific models, determine which use case each one fits, discuss the pros and cons of using that model...]]></description><link>https://engineering.cloudflight.io/open-llms-real-results-a-series-on-free-to-use-ai-models</link><guid isPermaLink="true">https://engineering.cloudflight.io/open-llms-real-results-a-series-on-free-to-use-ai-models</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Vlad Teodorescu]]></dc:creator><pubDate>Wed, 27 Aug 2025 08:55:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/_0iV9LmPDn0/upload/9b19a72297c2dc14b566423f10b9d8c6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Part 1 - An Introduction to the matter</p>
<h2 id="heading-what-will-this-series-be-about">What will this series be about?</h2>
<p>During this series of blog posts, I will do my best to provide my opinion on specific models, determine which use case each one fits, discuss the pros and cons of using that model, and more. I’d like to start with some general notions and first steps, and then deep-dive into different models and particularities of those models. Disclaimer: All opinions on usage and prompts are from personal experience with these models.</p>
<h2 id="heading-what-prompted-this">What prompted this?</h2>
<p>Year 2025 has been a year for new beginnings in my life. Among these are new personal projects. And the frustrations that sometimes come with them. During a rant to a friend about such frustrations (looking at you, configuration issues), he asked me if this is the tipping stone and if I will finally give ChatGPT a chance.</p>
<p>Now, when it comes to technology, especially software, I am one of those people who want to try the new shiny thing as soon as possible and test the limits. However, there is one piece of tech I avoided since it came out: LLMs. I simply did not believe that technology was there. However, I was proven at least partially wrong when I finally tried it for my personal project.</p>
<h2 id="heading-data-privacy-and-mistakes-or-why-i-avoided-llms-for-so-long">Data privacy and mistakes, or why I avoided LLMs for so long</h2>
<p>One of the main issues I had with LLMs is that their data privacy rules were not super clear in the beginning. In an ideal world, such information would be only used to provide the answer and, for longer conversations, to be saved locally on the device and referenced only in the context of that conversation.</p>
<p>However, that has somewhat improved in recent times. For example, OpenAI, Microsoft, and Anthropic do not use the prompts and generated answers for training, while others, like Google, gate this control behind a paywall. Or, in the case of DeepSeek, don’t even mention an opt-out feature.</p>
<p>If we look at how the data is stored, we have the following cases:</p>
<ul>
<li><p>OpenAI - 30 days</p>
</li>
<li><p>Microsoft - configurable or ZDR (Zero Data Retention)</p>
</li>
<li><p>Google - 3 years if randomly selected for human review. 3 days with activity tracking off</p>
</li>
<li><p>Anthropic - unspecified</p>
</li>
<li><p>DeepSeek - unclear</p>
</li>
</ul>
<p>Then, there is the issue of ownership over the prompts and results. OpenAI, Anthropic, and Microsoft specify that the user has ownership, while Google imposes some restrictions (the data of free-tier users can be accessed by others and is used in training). DeepSeek is again vague, since it uses user data by default for improving the model. There is one thing all the models used as examples above agree on: no copyright guarantee.</p>
<p>The second issue that plagues LLMs is hallucinations and the mistakes resulting from that (how many rocks did I need to eat? One small rock per day?). So, never take what an LLM says for granted.</p>
<h2 id="heading-what-is-better-online-hosted-or-self-hosted">What is better? Online hosted or self-hosted?</h2>
<p>While there are more ways to use an LLM other than Online and Self-hosting, these are probably the most accessible to the average user. So I will focus on these methods at the moment.</p>
<p>I will start with what I would call online hosting. This is the method in which you use the LLM the way it is provided by the creator company, using their apps or websites with an account created and with the settings tuned to your needs.</p>
<p>This is the easiest way, and it is great if you need speed, want to have state-of-the-art outputs, and zero overhead. However, there are drawbacks. You can’t guarantee privacy, you will need permanent network access, and you are always going to worry about your token limit. And sometimes things might just not work, and you will need to wait for some internal service to be back online.</p>
<p>Self-hosting, on the other hand, fixes some of these issues: in terms of privacy and network connection, you are in control. And in some cases, you can even tune the weights. But the machine or machines these run on need to have some competent hardware, the setup time might not be as fast, and it is unlikely that the self-hosted model is state-of-the-art.</p>
<p>So, in conclusion, every person who wants to start using LLMs in their daily life or work will have to decide the best way of accessing them. For me and my project, self-hosting will be the way forward.</p>
<h2 id="heading-whats-next">What’s next?</h2>
<p>Next time, we’ll talk about the first model that I had some more serious contact with: GPT-3.5. I know this is not an open model, but we will use this as a comparison base for the rest. And, additionally, we will discuss the first steps of self-hosting.</p>
]]></content:encoded></item><item><title><![CDATA[A fresh breath of air: what's new in Apache Airflow 3.0]]></title><description><![CDATA[Apache Airflow 3.0 has officially landed, and it represents a substantial evolution of the platform since its inception. This release is not just a collection of incremental changes, it’s a rethinking of how workflows can and should be managed at sca...]]></description><link>https://engineering.cloudflight.io/a-fresh-breath-of-air-whats-new-in-apache-airflow-30</link><guid isPermaLink="true">https://engineering.cloudflight.io/a-fresh-breath-of-air-whats-new-in-apache-airflow-30</guid><category><![CDATA[data-engineering]]></category><category><![CDATA[airflow]]></category><category><![CDATA[Data orchestration]]></category><category><![CDATA[release]]></category><dc:creator><![CDATA[Markus Thaler]]></dc:creator><pubDate>Mon, 11 Aug 2025 06:45:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753864915088/290a8168-621f-45c0-84d2-f97812e4151c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Apache Airflow 3.0 <a target="_blank" href="https://airflow.apache.org/docs/apache-airflow/stable/installation/upgrading_to_airflow3.html">has officially landed</a>, and it represents a substantial evolution of the platform since its inception. This release is not just a collection of incremental changes, it’s a rethinking of how workflows can and should be managed at scale. Packed with performance enhancements, quality-of-life improvements, and forward-looking features, Airflow 3.0 aims to address many of the longstanding frustrations within the data engineering community while keeping pace with modern orchestration needs. However, there’s still room for refinement. Deployment remains clunky, the developer experience could be more intuitive, and the documentation has yet to fully catch up with the new features, leaving gaps in usability. Despite these challenges, Airflow 3.0 lays a strong foundation for future innovation.</p>
<h2 id="heading-a-new-foundation-for-workflow-orchestration">A New Foundation for Workflow Orchestration</h2>
<p>The biggest additions of Airflow 3.0 at a glance:</p>
<ul>
<li><p>Event-based triggers</p>
</li>
<li><p>Workflow (DAG) versioning</p>
</li>
<li><p>New react-based UI</p>
</li>
<li><p>Asset notation</p>
</li>
<li><p>Backfills</p>
</li>
</ul>
<p>One of the most anticipated additions to Airflow 3.0 is its support for <strong>event-driven scheduling</strong>. Previously, Airflow’s strength was in time-based, cron-style orchestration. While this model works well for batch pipelines, it struggles in scenarios where real-time responsiveness is critical. With 3.0, workflows can now respond to data events, such as files appearing in cloud storage buckets or updates occurring in databases, enabling near-real-time orchestration. This positions Airflow to handle streaming and micro-batch use cases more elegantly than ever before (<a target="_blank" href="https://www.datacamp.com/blog/apache-airflow-3-0">DataCamp</a>).</p>
<p>Another major advancement is <strong>built-in DAG versioning</strong>, which allows every execution of a Directed Acyclic Graph (DAG) to be tied to a specific, immutable snapshot of its definition. This feature significantly improves debugging, traceability, and auditing, particularly for organizations in regulated industries where compliance and reproducibility are crucial. The versioning helps answer the critical question of what code is executed at which point in time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753866573258/d512ca29-5bdb-4823-9360-4bcefca23108.gif" alt class="image--center mx-auto" /></p>
<p>Airflow 3.0 also comes with a <strong>completely overhauled web UI</strong>, rebuilt using modern frontend technologies. The new interface delivers faster performance and a cleaner user experience. With improved tools for visualizing DAG runs, managing tasks, and inspecting logs, the user interface is no longer a pain point, but a productivity enhancer.</p>
<p>Furthermore, the new <strong>asset-centric syntax</strong> enables developers to use the <code>@asset</code> decorator to define workflows directly around data assets. This reduces boilerplate and aligns pipeline logic more naturally with the data itself. In practice, it shifts the paradigm from orchestrating tasks to orchestrating data—a subtle but meaningful conceptual leap.</p>
<p>Lastly, <strong>scheduler-managed backfills</strong> eliminate the hassle of managing historical data reprocessing through fragile CLI commands. Backfills can now be triggered, paused, and monitored directly from the UI or API, dramatically simplifying historical data correction.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753867168041/df34c43e-fab3-4bfe-9b9f-5a1fca19c2f6.gif" alt class="image--center mx-auto" /></p>
<p>Apart from newly added features, Airflow 3.0 also brings some improvements under the hood. Astronomer.io, a key driving force behind Airflow’s ongoing development, played a crucial role in the rollout of Airflow 3.0. As detailed in their <a target="_blank" href="https://www.astronomer.io/airflow/3-0/intro/">overview</a>, the most substantial optimizations include:</p>
<ul>
<li><p><strong>A Faster Scheduler:</strong> The Airflow 3.0 scheduler is optimized for speed and scalability, reducing latency during DAG processing and enabling faster task execution feedback.</p>
</li>
<li><p><strong>Active Dependency Management:</strong> Improved dependency tracking increases responsiveness and execution efficiency, particularly for complex pipelines.</p>
</li>
<li><p><strong>Database Connectivity Improvements:</strong> Airflow now interacts with metadata databases more efficiently, improving stability and reducing load on the backend.</p>
</li>
<li><p><strong>Upgrade Path Enhancements:</strong> Astronomer has simplified the upgrade path for both self-hosted and managed Airflow environments, making it easier for organizations to move from previous versions to 3.0.</p>
</li>
</ul>
<h2 id="heading-advantages-that-elevate-the-experience">Advantages That Elevate the Experience</h2>
<p>The improvements in Airflow 3.0 offer several benefits. Not only the changes in the architecture, but also the updated UI enhance observability and performance, giving engineers better insight into the health of their workflows through logs, metrics, and task statuses.</p>
<p>Airflow’s maturity continues to be one of its biggest strengths. With wide adoption across industries and robust community support, organizations can confidently deploy and scale their pipelines using a platform with proven reliability. Integration with other tools, like dbt (data build tool), remains strong, allowing users to orchestrate transformations seamlessly in concert with data ingestion and extraction workflows (see: <a target="_blank" href="https://medium.com/google-cloud/execute-dbt-with-airflow-and-cloud-run-fdf02571f394">Execute DBT with Airflow and Cloud Run</a>).</p>
<h2 id="heading-pain-points-that-still-remain">Pain Points That Still Remain</h2>
<p>Despite the significant improvements introduced in Airflow 3.0, the platform still carries several challenges that may hinder its adoption and use. One major issue is its <strong>steep learning curve</strong>, which remains a roadblock for many teams. Configuring DAGs, setting up deployment infrastructure, and troubleshooting failures often require deep technical expertise and careful coordination between multiple components.</p>
<p>Running Airflow at scale continues to be <strong>a resource-intensive undertaking,</strong> particularly for organizations managing hundreds or thousands of orchestrated tasks per day. Additionally, while Airflow 3.0 has taken strides forward, it still doesn’t fully embrace <strong>streaming workflows</strong> yet.</p>
<p>Historically, Airflow has faced criticisms related to scheduler instability, DAG deadlocks, and challenges with local testing—problems often discussed in forums like <a target="_blank" href="https://www.reddit.com/r/dataengineering/comments/1ijtt2b/why_dagster_instead_airflow/">this Reddit thread</a>. Although some of these issues have been alleviated in version 3.0, others persist and remain in areas that require further focus and improvement.</p>
<h3 id="heading-challenges-specific-to-airflow-30">Challenges Specific to Airflow 3.0</h3>
<p>While Airflow 3.0 introduces several new features, many are underdeveloped and fall short of being ready for seamless use. For instance, the implementation of <strong>Asset adapters</strong> and <strong>Event-based triggers</strong> appears incomplete. These much-anticipated features lack polish for seamless developer experience, by only supporting a limited amount of connectors right out of the box. Similarly, the UI for visualizing <strong>Assets</strong> and <strong>DAGs</strong> can be confusing, leaving users struggling to fully understand or manage the relationships between these components.</p>
<p>Another pain point relates to <strong>DAG versioning</strong>, which is unable to effectively track changes to <strong>dbt models</strong> using cosmos - a capability that many may find invaluable.</p>
<p>The overall <strong>developer experience</strong> in Airflow 3.0 also continues to feel clunky. From initial setup to debugging, developers starting out with Airflow may struggle with the platform’s tools and processes. This is coupled with the <strong>complex nature of deployments</strong>, which are especially challenging in advanced scenarios, such as setting up Airflow on Kubernetes or managing hybrid-cloud environments.</p>
<p>One of the most noticeable drawbacks of Airflow 3.0 is the <strong>insufficient or missing documentation</strong> for its new features. Teams are left without clear guidance on several critical areas, such as:</p>
<ul>
<li><p>How to <strong>connect Assets</strong> with classic DAGs.</p>
</li>
<li><p>How to implement and use <strong>Asset adapters</strong>.</p>
</li>
<li><p>How to configure and utilize <strong>Event-based triggers</strong> effectively.</p>
</li>
</ul>
<p>This lack of comprehensive documentation creates a barrier for teams trying to explore and adopt the new functionalities introduced in Airflow 3.0. Without practical examples or detailed tutorials, understanding the usage of these features from the source code can be cumbersome.</p>
<h2 id="heading-comparing-airflow-30-to-other-orchestration-tools">Comparing Airflow 3.0 to Other Orchestration Tools</h2>
<p>In the rapidly evolving world of orchestration tools, how does Airflow 3.0 stack up?</p>
<p><strong>Prefect</strong> has long marketed itself as a more Pythonic and lightweight alternative to Airflow. It offers seamless local development, easy debugging, and a simple function-based interface. For teams seeking quick setup and modern developer experience, Prefect is hard to beat. However, Prefect may fall short in highly complex enterprise environments where Airflow’s fine-grained scheduling and extensibility are still unmatched. In <a target="_blank" href="https://medium.com/@lasyachowdary1703/day-19-prefect-vs-apache-airflow-choosing-the-right-data-orchestration-tool-12c8e47c58c6">comparison</a>, Prefect is easier to use, Airflow’s integrations and broader community support make it more suitable for complex, regulated workflows.</p>
<p><strong>Dagster</strong>, by contrast, places a heavy emphasis on data assets and observability. It provides a more modular, testable, and development-friendly interface for building pipelines. Dagster’s partitioning mechanism, better support for local development, and clear delineation between production and staging make it a favorite among data teams with engineering-heavy workflows. Airflow 3.0 narrows this gap considerably with its asset-centric features and improved developer tooling, but Dagster still feels more modern in its architecture. Still, the smaller community and the missing RBAC support for the open source version might be a deal breaker for some projects.</p>
<p>Other tools like <strong>Kestra</strong>, <strong>Shipyard</strong>, and <strong>DataChannel</strong> each have their own niches, often targeting ease-of-use or native SaaS integrations, but they don’t yet match Airflow’s flexibility. <strong>Azure Data Factory (ADF)</strong> offers strong native integration with the Azure ecosystem and is ideal for Microsoft-heavy shops, though it lacks the open-source extensibility that defines Airflow.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Apache Airflow 3.0 is a big release that reaffirms the platform’s relevance and adaptability. It addresses many of the performance, usability, and reliability issues that have long plagued the system, while introducing forward-looking features that make it competitive with modern orchestrators like Prefect and Dagster.</p>
<p>While the platform still demands operational expertise and thoughtful architecture to run effectively, it now offers a significantly better out-of-the-box experience. For organizations that need to scale complex workflows with a high degree of control and visibility, Airflow 3.0 offers one of the most mature and capable orchestration solutions on the market today.</p>
<p>As mentioned, there are areas that still need attention from the Airflow community. The immaturity of new features, the incomplete documentation, and the ongoing challenges with deployment complexity and usability can create frustration for teams. For organizations already invested in Airflow, these might be manageable issues, but for those evaluating orchestration tools, they could serve as deterrents. In some cases, tools like Dagster or Prefect, with their more intuitive workflows and seamless integrations, may still be more attractive alternatives. Airflow 3.0 has set a strong foundation for future growth, but there is still work to be done to make it a truly developer-friendly and scalable solution.</p>
]]></content:encoded></item><item><title><![CDATA[Oh yes, new Angular Material 19! Wait, oh NO!]]></title><description><![CDATA[Yes, a new Angular Material 19 is now available to complement Angular 19 itself. As always, I'm eager to update as soon as possible because we all want to see it shine!
While I have no doubt that the new Angular Material is great, there is one aspect...]]></description><link>https://engineering.cloudflight.io/oh-yes-new-angular-material-19-wait-oh-no</link><guid isPermaLink="true">https://engineering.cloudflight.io/oh-yes-new-angular-material-19-wait-oh-no</guid><category><![CDATA[material theme]]></category><category><![CDATA[Angular]]></category><category><![CDATA[Material Design]]></category><category><![CDATA[material]]></category><category><![CDATA[prefers color scheme]]></category><category><![CDATA[macOS]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Ondrej Oravčok]]></dc:creator><pubDate>Fri, 28 Feb 2025 11:19:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/vPXP2Kgo_rY/upload/13c2104dd27cabd33e287ad0bdd24011.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Yes, a new Angular Material 19 is now available to complement Angular 19 itself. As always, I'm eager to update as soon as possible because we all want to see it shine!</p>
<p>While I have no doubt that the new Angular Material is great, there is one aspect that might surprise you later. In this article, I would like to share with you my experience.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739279920344/b04cd09a-c39b-405c-bfd8-81dc4c7f55eb.png" alt="ondrej shakespeare: to light or to dark? that is the question" class="image--center mx-auto" /></p>
<h2 id="heading-is-it-easy-to-update-to-material-19-from-the-previous-version">Is it easy to update to Material 19 from the previous version?</h2>
<p>No, not at all. But we got used to it recently, didn't we? Joking aside, there is a guide and also a migration script you can perform via <code>ng update</code>, but definitely some manual intervention will be needed.</p>
<h2 id="heading-angular-material-19-what-is-so-different">Angular Material 19 - What is so different?</h2>
<p>In the new version, it is the theming that makes the difference. Theming is what "<em>makes you customize colors and typography</em>"[<a target="_blank" href="https://material.angular.io/guide/theming">1</a>] in your app, and while Angular Material 17 works with M2 (Material Design 2), Angular Material 18 and 19 work with M3 (Material Design 3).</p>
<p>I really think the new M3 works pretty well, but to help people understand the idea behind it, there are often nice parts in the documentation that compare M3 to M2 when explaining things like <a target="_blank" href="https://m3.material.io/styles/elevation/overview#f9947307-4818-4d94-b98a-fa1cb5498eb1">color roles or elevation effects.</a></p>
<h2 id="heading-past-and-present-practical-examples">Past and Present - practical examples</h2>
<p>How we used to define themes in Angular Material 17:</p>
<pre><code class="lang-scss"><span class="hljs-keyword">@use</span> <span class="hljs-string">'@angular/material'</span> as mat;

<span class="hljs-variable">$my-primary</span>: mat.define-palette(mat.<span class="hljs-variable">$indigo-palette</span>, <span class="hljs-number">500</span>);
<span class="hljs-variable">$my-accent</span>: mat.define-palette(mat.<span class="hljs-variable">$pink-palette</span>, A200, A100, A400);

<span class="hljs-variable">$theme</span>: mat.define-light-theme((
 color: (
   primary: <span class="hljs-variable">$my-primary</span>,
   accent: <span class="hljs-variable">$my-accent</span>,
 ),
 // typography + density
));
</code></pre>
<p>How we used to define themes in Angular Material 18:</p>
<pre><code class="lang-scss"><span class="hljs-keyword">@use</span> <span class="hljs-string">'@angular/material'</span> as mat;

<span class="hljs-variable">$theme</span>: mat.define-theme((
  color: (
    theme-type: dark,
    primary: mat.<span class="hljs-variable">$violet-palette</span>,
  ),
  // typography + density
));
</code></pre>
<p>How it is done now in Angular Material 19:</p>
<pre><code class="lang-scss"><span class="hljs-keyword">@use</span> <span class="hljs-string">'@angular/material'</span> as mat;

<span class="hljs-selector-tag">html</span> {
  <span class="hljs-attribute">color</span>-scheme: light dark; <span class="hljs-comment">// can be light, dark, or both (like here)</span>
  <span class="hljs-keyword">@include</span> mat.theme((
    color: mat.<span class="hljs-variable">$violet-palette</span>,
    // typography + density
  ));
}
</code></pre>
<p>It may not look that different at first glance, but there are <strong>2 key things:</strong></p>
<ul>
<li><p>in v17 and v18 we store the created theme in the SASS variable <code>$theme</code>, we do not do that in v19</p>
</li>
<li><p>v19 uses <code>color-scheme</code>, whereas in v17 and v18 the theme itself is either <code>light</code> or <code>dark</code></p>
</li>
</ul>
<h2 id="heading-sass-variable-theme-is-not-needed-anymore">SASS variable $theme is not needed anymore</h2>
<p>Prior to v19, referencing themes and colors was not ideal. Anytime we needed to get a color from a theme and use it, we had to find/import the created theme + also import material tools, and then use them to extract some color:</p>
<pre><code class="lang-scss"><span class="hljs-keyword">@use</span> <span class="hljs-string">'@angular/material'</span> as mat;
<span class="hljs-keyword">@use</span> <span class="hljs-string">'theme'</span> as theme;

<span class="hljs-selector-class">.color-primary</span> {
  <span class="hljs-attribute">color</span>: mat.get-color-from-palette(map-get(<span class="hljs-variable">$theme</span>, primary));
}
</code></pre>
<p>What happens inside the v19 theme definition is the creation of globally accesible properties. So the same code looks like this:</p>
<pre><code class="lang-scss"><span class="hljs-selector-class">.color-primary</span> {
  <span class="hljs-attribute">color</span>: var(--mat-sys-primary);
}
</code></pre>
<p>This is much nicer as we do not need a huge amount of references and <code>@import</code>/<code>@use</code>/<code>@forward</code> rules just to get some color. You can read about all possible color roles in the <a target="_blank" href="https://m3.material.io/styles/color/roles#e9fc5b00-8355-4641-b35f-58b0bac639f3">M3 documentation</a>.</p>
<h2 id="heading-css-color-scheme-vs-explicit-lightdark">CSS color-scheme vs explicit light/dark</h2>
<p><code>color-scheme</code> is a CSS property that allows html elements to indicate which color scheme to use - if <code>dark</code> or <code>light</code></p>
<p>Although this is nothing new, I often find this feature missing, struggling with dark pages during the day, or bright screens at night. In my personal opinion this can give the user a much better experience, and here we have all the tools to do it, so please:</p>
<blockquote>
<p>Never hardcode colors!!!</p>
</blockquote>
<p>Now we all know that we can easily create our pages to support light/dark themes automatically, but then why is there “Oh NO“ in the title?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739280103193/a1fb9074-f526-497e-ae11-47befc28c909.png" alt="ondrej anakin: is everything fine with this color scheme?" class="image--center mx-auto" /></p>
<p>Yes, everything is fine with this color-scheme, the reason is a CSS function called <code>light-dark</code> on which all this functionality is based on.</p>
<h2 id="heading-light-dark">light-dark</h2>
<p><code>light-dark</code> is a CSS function that simply selects 1 color out of 2 based on - yes, you are right - based on <code>color-scheme</code>. So if you inspect your browser, you will see, that <code>var(—mat-sys-primary)</code> (which we used to set the color) does not hold <code>#005cbb</code>, but it holds <code>light-dark(#005cbb, #abc7ff)</code>.</p>
<p>All is well unless you check the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark#browser_compatibility">compatibility table</a> for the <code>light-dark</code> function. I did not do that. Until someone opened my page on a Macbook and reported bug.</p>
<p>Long story short - <code>light-dark</code> on Apple devices is only available from 17.5, so the probability that someone e.g. does not have the latest iOS is high.</p>
<h1 id="heading-it-was-a-nice-try-but-should-we-revert-now">It was a nice try, but should we revert now?</h1>
<p>No, there is still a way!</p>
<p>We can still combine the old way of defining themes and reading scheme preferences with the new Angular Material 19. My solution to this problem was the following:</p>
<pre><code class="lang-scss"><span class="hljs-selector-tag">html</span> {
  <span class="hljs-keyword">@include</span> mat.theme((
    color: (
      primary: mat.<span class="hljs-variable">$violet-palette</span>,
      theme-type: light,
    ),
    // typography <span class="hljs-keyword">and</span> density
  ));
}

<span class="hljs-keyword">@media</span> (prefers-color-scheme: dark) {
  <span class="hljs-selector-tag">html</span> {
    <span class="hljs-keyword">@include</span> mat.theme((
      color: (
        primary: mat.<span class="hljs-variable">$violet-palette</span>,
        theme-type: dark,
      ),
      // typography <span class="hljs-keyword">and</span> density
    ));
  }
}
</code></pre>
<p>Thanks to a CSS media feature called <code>prefers-color-scheme</code>, we can achieve exactly the same behavior, only with much less compatibility issues, as this is supported back to iOS 13.</p>
<p>The reason for this is that if we define theme as 1-color, globally accessible properties like <code>var(--mat-sys-primary)</code> will not evaluate to a function holding both colors like <code>light-dark(#005cbb, #abc7ff)</code>, but to color directly like<code>#005cbb</code>.</p>
<h1 id="heading-please-do-not-test-in-production">Please do (not) test in production!</h1>
<p>After the time I invested in switching all the M2 code to M3, I was pretty disappointed to run into such a problem, but thanks to my great colleagues at <a target="_blank" href="https://career.cloudflight.io/jobs">Cloudflight</a>, we were able to quickly find a good solution to this problem while still staying with Angular Material 19.</p>
<p>Although <strong>no users were harmed during this experiment</strong>, please, do not test in production. It's dark. But yeah, dark is not bad if the user requested it like that 😁😁😁</p>
<p>Thanks for reading.</p>
]]></content:encoded></item><item><title><![CDATA[How to do effective Requirements Engineering in Fixed-Price and Fixed-Scope Projects]]></title><description><![CDATA[If you're working as a service provider for companies looking to outsource their product development, you may encounter customers who have a set of requirements from the beginning and are ready to negotiate a fixed price right from the start. When me...]]></description><link>https://engineering.cloudflight.io/how-to-do-effective-requirements-engineering-in-fixed-price-and-fixed-scope-projects</link><guid isPermaLink="true">https://engineering.cloudflight.io/how-to-do-effective-requirements-engineering-in-fixed-price-and-fixed-scope-projects</guid><category><![CDATA[fixedprice]]></category><category><![CDATA[requirements engineering]]></category><category><![CDATA[requirements]]></category><category><![CDATA[pros and cons]]></category><category><![CDATA[projects]]></category><category><![CDATA[project management]]></category><dc:creator><![CDATA[Róbert Kovács]]></dc:creator><pubDate>Thu, 30 Jan 2025 08:04:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/5fNmWej4tAA/upload/311a5bbecf037939377ecc51346429b2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're working as a service provider for companies looking to outsource their product development, you may encounter customers who have a set of requirements from the beginning and are ready to negotiate a fixed price right from the start. When meeting such a customer, it's crucial to handle the situation so that the customer is fully satisfied and you can stay within the budget.</p>
<p>This blog post aims to serve as both an introduction and a set of suggestions to help you manage such projects and customers regardless of whether you are a Requirements Engineer, Business Analyst or a Project Manager. On the other side, it aims to support customers with a bit of insight into how fixed priced projects are best handled, so that they can collaborate more effectively.</p>
<p>Before we get into it, a quick introduction is needed of the subject matter.</p>
<h1 id="heading-what-does-fixed-priced-mean">What does Fixed Priced mean?</h1>
<p>A fixed price software project is a type of contractual agreement where the client and the service provider agree on a set price for the entire project before any work begins. This price remains constant regardless of the actual time or resources expended to complete the project. The scope, deliverables, and timelines are clearly defined and agreed upon in advance.</p>
<h1 id="heading-pros-and-cons">Pros and cons</h1>
<p>There are several benefits and risks when getting into such a project:</p>
<h2 id="heading-pros">Pros</h2>
<ul>
<li><p>Customer defines scope in advance and knows exactly what they will receive</p>
</li>
<li><p>Customer knows the timing of the delivery, and can plan work around that delivery</p>
</li>
<li><p>If the estimation is accurate, the service provider knows how many resources they need to allocate</p>
</li>
<li><p>Simple, low complexity projects with a small time frame are a great option as it helps keep the focus and achieve the end goal</p>
</li>
</ul>
<h2 id="heading-cons">Cons</h2>
<ul>
<li><p>Delivery and value of the software is fully dependent on the quality of the predefined requirements, deliverable descriptions</p>
</li>
<li><p>Little room for including additional functionalities that are discovered during development</p>
</li>
<li><p>Fixed Price projects are usually handled with a Waterfall approach which has proven to be extremely risky, especially for the customer</p>
</li>
<li><p>Cost estimation is fully based upon an initial understanding of the customer needs. During development this understanding may evolve, thus requiring more effort</p>
</li>
<li><p>Delays have a compounding effect. If the project plan assumes the acceptance of a deliverable by a specific point of time which is not met, then the whole project timeline may be shifted</p>
</li>
<li><p>The fixed scope nature makes it difficult to adapt to changes in the business environment</p>
</li>
<li><p>Large scale projects have too many risks associated</p>
</li>
</ul>
<p>After looking at all these pros and cons, several steps can be taken to manage them well.</p>
<h1 id="heading-project-lifecycle">Project Lifecycle</h1>
<p>Below you can find several suggestions on what you can do to support the Project setting in the Project Lifecycle</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738223887318/e1e2aaae-01f8-4e94-a724-f74080fbd4d0.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-estimations">Estimations</h1>
<p>As with all software projects, an accurate estimation means a successful delivery, happy customer and a project that has stuck to the planned budget.</p>
<p>Depending on the scope size though, the estimation accuracy may be reduced because of which buffers have to be introduced to support the increasing risk size.</p>
<p>In order to manage that, the following measures can be taken:</p>
<ul>
<li><p>Splitting of the scope into multiple smaller scopes that can be more easily handled, estimated and delivered</p>
</li>
<li><p>Separating the requirements of the customer into risk categories, and either descoping or closely managing high-risk requirements</p>
</li>
<li><p>In case the scope is too big, and separation is not possible, additional budget can be negotiated for design and/or in-depth analysis of requirements be. This should provide with an increased insight into the scope to be able to do a more accurate estimation</p>
</li>
<li><p>If none of the above is possible, it should be communicated to the customer that a safety buffer has to be applied to manage all the unforeseen risks.</p>
</li>
</ul>
<p>A successful estimation is one where you, as Requirements Engineer, feel confident in having an extremely good understanding of what the scope entails and both you and the customer are satisfied with the budget.</p>
<p>Additionally to understanding the scope, consider effort for the following tasks:</p>
<ul>
<li><p>Project Setup</p>
</li>
<li><p>Testing</p>
</li>
<li><p>Meetings</p>
</li>
<li><p>Documentation</p>
</li>
<li><p>Clarifications, negotiations, defect and requirement analysis</p>
</li>
</ul>
<p>In our experience this usually is between 30-50% of additional time, depending on the customer.</p>
<h1 id="heading-design-stage">Design Stage</h1>
<p>Once the customer has agreed to the Cost Estimation of the work scope, the Design can start.</p>
<p>During the design phase, alongside the usual design process of clarification, backlog planning, and user story preparation, it is crucial for an RE to document and clearly communicate what the customer <strong>will not receive</strong>. It's also important to record their agreement to this. This will make life during delivery exponentially easier for both parties.</p>
<p>As the requirements received from the customer are the cornerstones of the design, a core part of the Design Stage is making sure that there is a common agreement on what those requirements mean, usually in a documented form to further reduce potential misunderstandings, and also to crystallize the scope and design.</p>
<p>Depending on the quality of the initially drafted requirements, a process of rewriting, nomenclature alignment, and acceptance criteria polishing is advised.</p>
<p>The customer may or may not have a product owner that can support during planning. The luxury of Fixed Price, Fixed Scope projects is that the order of implementation is usually not imposed by the customer. This way there is room for prioritization not by value (as all requirements are equally valuable in terms of the contract), but by logical, technical dependencies to make implementation as streamlined as possible.</p>
<p>During the design stage it can also be a good idea to already discuss the process of the customer requesting additional scope. The requests will inadvertently happen once the customer starts using different draft versions of the application and realizes that the requirements have not exactly reflected what they wanted due to human error, interpretation issues, language barriers, etc.</p>
<p>In these situations it is important to</p>
<ul>
<li><p>Reinforce the customer and the willingness to provide the best software product</p>
</li>
<li><p>Highlight the limitations of the contract</p>
</li>
<li><p>Discuss next steps in order to be able to deliver the requested feature i.e. in a different contract or by descoping something else</p>
</li>
<li><p>Refer to the process you have agreed upon in the Design Stage</p>
</li>
</ul>
<p>Further details about handling scope changes will be explained below.</p>
<h1 id="heading-delivery">Delivery</h1>
<h2 id="heading-waterfall-vs-agile">Waterfall vs Agile</h2>
<p>An assumption that is usually made when discussing the delivery of fixed price, fixed scope projects is that the customer has communicated everything, you have all the requirements available, thus customer involvement is not necessary until the full scope has been implemented.</p>
<p>As proven thousands of times, this approach of waterfall does not work in software projects due to the sheer complexity these products usually have.</p>
<p>If the customer wants to have exactly what they want, the way they want it, they will need to involve themselves actively during development. Usually, a Product Owner can support the development process with in-depth knowledge, which can significantly speed up decision-making. Alternatively, a representative of the end users should be available to the development team to assist in making informed decisions that best serve the customer.</p>
<h2 id="heading-delivery-process">Delivery Process</h2>
<p>In the process of delivery, the usual Agile approach shall be taken with a few additional pre-steps and post-steps.</p>
<p>The usual Agile approach with Scrum consists of the four agile ceremonies: Sprint Planning, Daily Standup, Sprint Review, Sprint Retrospective. These should be enough to facilitate a healthy iterative delivery to fully handle and develop the backlog items.</p>
<p>Next to this, the following auxiliary meetings are advised:</p>
<ul>
<li><p>Before the sprint planning, review the scope of the requirements and clarify both what they customer will receive and what they will not</p>
</li>
<li><p>A weekly meeting with the customer to discuss any additional open topics, questions or defects</p>
</li>
<li><p>A weekly meeting with the decision maker on the defects, potential additional scope, project health</p>
</li>
</ul>
<p>It can also have a great benefit to provide the customer as early as possible with mockups or small PoC solutions so that they can decide whether the approach you would like to implement would actually work for them. Help the customer make the decision by also highlighting what the consequences could be, both positive and negative, so they can make informed choices.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737983599621/f4708059-b5b8-4114-90a9-5e04d31e6c16.png" alt="Scrum Ceremonies extended with Auxiliary meetings" class="image--center mx-auto" /></p>
<h2 id="heading-defects-handling">Defects Handling</h2>
<p>After each iteration of delivery, it is expected that the customer tests the delivered requirements. During this testing process, defects can and will appear, that can be of the following categories:</p>
<ul>
<li><p>Defects that clearly don’t satisfy one or more requirements</p>
</li>
<li><p>Defects that don’t satisfy the spirit of a requirement</p>
</li>
<li><p>Defects that are additional scope</p>
</li>
</ul>
<p>Whoever analyzes these defects, their first task should be correctly assigning the defect into one of these buckets first, then providing an appropriate answer.</p>
<ul>
<li><p>In the case of clear defects, they should be planned for a further Sprint</p>
</li>
<li><p>In the case of grey area defects, a discussion shall be had on the value vs cost. If the cost/value seems too high, the defect needs to be descoped</p>
</li>
<li><p>Defects that are additional scope, or new feature requests should be assigned to a later contract.</p>
</li>
</ul>
<p>Although, intuitively we want to deliver the best software we can for our customer, and solve all their problems, there simply is not enough budget to be able to do that.</p>
<h1 id="heading-contract-closure">Contract Closure</h1>
<p>Hopefully, by following a rigorous delivery approach, the delivery of the full scope within the budget was possible. Depending on the timeframe of the interaction, several outputs and outcomes were achieved, upon which you should be able to build:</p>
<p>Outputs:</p>
<ul>
<li><p>Documentation</p>
</li>
<li><p>Software Code and functionality</p>
</li>
<li><p>Testing supporting the functionality (either history of testing or automated test plans)</p>
</li>
<li><p>Backlog of unresolved defects / tasks that have been defined out of scope</p>
</li>
</ul>
<p>Outcomes:</p>
<ul>
<li><p>A very well defined and oiled delivery process</p>
</li>
<li><p>Great interaction and involvement from the customer</p>
</li>
<li><p>Understanding of additional effort needed next to the requirements</p>
</li>
</ul>
<p>You can use these as inputs to the following steps:</p>
<ul>
<li><p>The backlog of defects/tasks can be an opportunity for a new contract, polishing the application, applying improvements</p>
</li>
<li><p>Building upon the already existing processes they can rest assured that delivery will be quicker than with other providers</p>
</li>
<li><p>Future estimations will be more accurate with the same customer</p>
</li>
</ul>
<h1 id="heading-closing-thoughts">Closing Thoughts</h1>
<p>Whenever working in such a restricted context, it is important to always be reminded of the rules of the game. Although the natural human behavior is to support the customer and make their lives easy, this may be counteractive to the goals of finishing on scope and in budget.</p>
<p>Being able to provide affordable alternatives and handle difficult situations in Fixed Price projects is the bread and butter of a requirements engineer. The customer may request something completely outside the scope, and it is essential to stay open minded and try to support them with ideas that can still fit into the scope. As a last resort there is always the option of saying a well framed “no” with the goal of reminding the limitations of the contract setting.</p>
<p>Agreements on processes should be made early to avoid surprises and negative emotions on either side.</p>
<p>Handling such projects can be tough but extremely rewarding if you can stick to the initial planning.</p>
]]></content:encoded></item><item><title><![CDATA[Language Bias in Multilingual Semantic Search Systems]]></title><description><![CDATA[In the fast-changing field of natural language processing (NLP), semantic search has become crucial for applications like chatbots and fact-checkers. This technology can also enhance search engines, allowing users to find relevant information by aski...]]></description><link>https://engineering.cloudflight.io/language-bias-in-multilingual-semantic-search-systems</link><guid isPermaLink="true">https://engineering.cloudflight.io/language-bias-in-multilingual-semantic-search-systems</guid><category><![CDATA[natural language processing]]></category><category><![CDATA[nlp]]></category><category><![CDATA[semantic search]]></category><category><![CDATA[Bias]]></category><category><![CDATA[multilingual]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[contrastive learning]]></category><category><![CDATA[NLU]]></category><category><![CDATA[Natural language understanding ]]></category><dc:creator><![CDATA[Johannes Vass]]></dc:creator><pubDate>Thu, 09 Jan 2025 12:27:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736419885930/223edbb0-41ab-414f-9a69-3f9d34e8dad9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the fast-changing field of natural language processing (NLP), semantic search has become crucial for applications like chatbots and fact-checkers. This technology can also enhance search engines, allowing users to find relevant information by asking questions in natural language. Embedding models are central to this, converting text into high-dimensional vectors that capture the meanings of words and phrases. This helps computers find the most relevant information by comparing similar vectors.</p>
<p>Cloudflight has a year of experience fine-tuning these models to make content retrieval for our customers more relevant. For example, we worked with the Austrian Press Agency (APA) on custom embedding models to improve knowledge discovery in the fast-paced news industry. In collaboration with the G39 group of European news agencies we are now making the developed system multi-lingual to allow users to retrieve relevant news in nine languages by querying in any language, making it easier to stay informed of reporting across different news agencies.</p>
<p>Multilingual semantic search isn't limited to news; it can also be useful in areas like international e-commerce or booking platforms. However, creating a truly multilingual system is challenging because models often prioritize results in the query's language. In this blog post, we aim to highlight this issue, explain it, and discuss how we mitigated it.</p>
<h2 id="heading-what-is-language-bias">What is Language Bias?</h2>
<p>Language bias occurs when multilingual semantic search systems prioritize results in the same language as the user query over results in other languages, even if the latter are more relevant. This bias is problematic, especially when the answer to a query is not available in the query language but exists in another language. In such cases, the system may return many irrelevant texts in the query language before showing the relevant ones in a different language. The term language bias was coined by Roy et al. (2020) in the paper <em>LAReQA: Language-agnostic answer retrieval from a multilingual pool</em> [2]. The authors define cross lingual alignment and describe two scenarios – weak and strong alignment:</p>
<p><strong><em>Weak Alignment*</em></strong>: For any item in language L1, the nearest neighbour in language L2 is the most semantically relevant item.*</p>
<p><strong><em>Strong Alignment*</em></strong>: For any item, all semantically relevant items are closer than all irrelevant items, regardless of their language. Crucially, relevant items in different languages are closer than irrelevant items in the same language.*</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729237368878/0d870164-c589-43fc-bf52-f64c3c827cdd.png" alt class="image--center mx-auto" /></p>
<p><em>Graphic taken from LAReQA: Language-agnostic answer retrieval from a multilingual pool [2]</em></p>
<p>Weakly aligned multi-lingual embedding models work well, when we want to use the same embedding model for each language content separately (e.g. I only want to host one embedding endpoint but have many content databases in different languages) or when I want to search in my language (e.g. German) in a French content database without translating the query.</p>
<p>However, if you need to perform semantic search in a content database containing texts in different languages, only models with strong alignment will produce results that are free of language bias, thus making sure that the most relevant candidates are ranked highest and not the ones matching the query language.</p>
<h2 id="heading-visualization-of-the-embedding-space">Visualization of the embedding space</h2>
<p>An easy way of detecting the presence of language bias in a multilingual model is to visualize the embeddings of a set of parallel sentences in two languages in 2D. That might look as follows, where the diagram shows embeddings of <a target="_blank" href="https://huggingface.co/intfloat/multilingual-e5-large">intfloat/multilingual-e5-large</a>, a very popular multilingual open-source model with language bias.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729237463352/0f6be583-d939-43c0-bf8e-4a35398f5ca0.png" alt class="image--center mx-auto" /></p>
<p>The diagram shows the data distribution in the model, where German data is shown in red and English data in blue. We can see that language is greatly influencing the embedding distribution. A strong separation between the coloured clusters means that there is a language bias.</p>
<p>In contrast, the following diagram shows blue and red points well interleaved, which means English and German embeddings map to the same semantic space and are not easily distinguished by language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729237470172/4b28bb6e-ce75-4663-8d4e-80baeddd43b1.png" alt class="image--center mx-auto" /></p>
<p>Beware that this visualization technique is only an illustrative starting point and does not allow us to quantify the degree of alignment.</p>
<h2 id="heading-how-to-measure-it">How to measure it?</h2>
<p>To quantify and compare the language bias of different models, you can follow one of these approaches:</p>
<p>One option is to evaluate your model on the LAReQA dataset as described in the paper <em>LAReQA: Language-agnostic answer retrieval from a multilingual pool</em> by Roy et al [2]. Besides a method for quantifying the bias, the paper proposes a useful heat map visualization where you can see the degree of alignment between all language pairs. On the downside, the LAReQA dataset does not consist of a lot of languages.</p>
<p>As an alternative, you can follow the approach from <em>Making Monolingual Sentence Embeddings Multilingual using Knowledge Distillation</em> by Nils Reimers and Iryna Gurevych [1]. They propose to evaluate the model on a semantic textual similarity (STS) task  using a multilingual STS dataset and see how much worse the model performs when you don’t test on every language individually but on a candidate set with all languages at once.</p>
<p>In addition to quantifiable measures, it’s also advisable to test for language bias manually. Select a couple of queries, translate them to different languages and issue a search for each translation. You should find approximately the same results for each translation of the same query. If you see results following the language of your query instead, you have a biased model without strong alignment.</p>
<h2 id="heading-mitigating-language-bias">Mitigating language bias</h2>
<p>For the G39 project we built up large fine-tuning datasets from news articles from different countries to let the model learn different news contexts. Due to our experience with training mono-lingual embedding models we were confident that we could create a model that knows the current news context of multiple countries. However, another central question was whether this trained model would be usable for multilingual semantic search. We researched and brainstormed many ideas, but the solution was surprisingly simple in the end: the baseline training approach produced satisfactory results already. That is, we merged our nine large monolingual finetuning datasets together into one consisting of queries and news article chunks in nine different languages (but each query-article-pair still used only language). Training on this dataset using a standard contrastive loss with in-batch negatives eliminated most of the language bias that we measured and negatively noticed in the base model.</p>
<p>Why does this work? We are still in the process of finding out, but we can already share two contributing factors: Firstly, having a large amount of high-quality multilingual fine-tuning data at our disposal, and secondly, having a considerable topic overlap between the different languages in the dataset, because a good part of the news is international.</p>
<p>We nicknamed our approach “brute-force mitigation”, since obviously having a large high-quality amount of training data did the trick. If you don’t have large amounts of multilingual finetuning data at hand but maybe just data in one language, the approach from Reimers and Gurevych [1] may be worth taking a look at. The paper explains how to extend the capabilities of an existing embedding model to new languages with a training objective that targets equal embeddings in all languages (i.e. strong alignment).</p>
<h2 id="heading-references">References</h2>
<p>[1] Nils Reimers and Iryna Gurevych, 2020, Making Monolingual Sentence Embeddings Multilingual using Knowledge Distillation <a target="_blank" href="https://arxiv.org/pdf/2004.09813">https://arxiv.org/pdf/2004.09813</a></p>
<p>[2] Roy et al., 2020, LAReQA: Language-agnostic answer retrieval from a multilingual pool <a target="_blank" href="https://arxiv.org/abs/2004.05484">https://arxiv.org/abs/2004.05484</a></p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Understanding the Essentials of Mobile Test Automation]]></title><description><![CDATA[Preface
When it comes to test automation in general, it's easy to say that it's complex. If we were to stick to the classic separation of the Test Pyramid, we'd end up with:
- Unit tests, which can be implemented for both backend and frontend.
- Inte...]]></description><link>https://engineering.cloudflight.io/understanding-the-essentials-of-mobile-test-automation</link><guid isPermaLink="true">https://engineering.cloudflight.io/understanding-the-essentials-of-mobile-test-automation</guid><category><![CDATA[test-automation]]></category><category><![CDATA[Webdriver.io]]></category><category><![CDATA[Mobile Test Automation]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[mobile app development]]></category><category><![CDATA[React Native]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Cordova]]></category><category><![CDATA[Ionic Framework]]></category><category><![CDATA[#maui]]></category><category><![CDATA[Xcode]]></category><category><![CDATA[Android Studio]]></category><category><![CDATA[Espresso Testing]]></category><category><![CDATA[appium]]></category><category><![CDATA[Detox]]></category><dc:creator><![CDATA[Tomasz Buga]]></dc:creator><pubDate>Wed, 08 Jan 2025 08:03:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/n31x0hhnzOs/upload/f5d12ebbb9cc065b7669be0f1f9ca558.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-preface"><strong>Preface</strong></h2>
<p>When it comes to test automation in general, it's easy to say that it's complex. If we were to stick to the classic separation of the <a target="_blank" href="https://martinfowler.com/articles/practical-test-pyramid.html">Test Pyramid</a>, we'd end up with:</p>
<p>- <strong>Unit tests</strong>, which can be implemented for both <em>backend</em> and <em>frontend</em>.</p>
<p>- <strong>Integration tests</strong>, which today can mean anything from <em>API testing</em>, <em>microservices</em> and <em>third-party service</em> <em>integration</em>, <em>user authentication</em> and <em>authorization</em>, to <em>quasi-performance testing</em>.</p>
<p>- <strong>End-to-end tests</strong>, which for the vast majority of test automation engineers and software developers would mean web application testing (to name a few major frameworks: <a target="_blank" href="https://playwright.dev/"><strong>Playwright</strong></a>, <a target="_blank" href="https://www.cypress.io/"><strong>Cypress</strong></a>, <a target="_blank" href="https://www.selenium.dev/"><strong>Selenium</strong></a>).</p>
<p>There are also tools like <a target="_blank" href="https://docs.pact.io/"><strong>Pact</strong></a> for <em>contract testing</em>, <a target="_blank" href="https://storybook.js.org/"><strong>Storybook</strong></a> for <em>advanced</em> <a target="_blank" href="https://storybook.js.org/docs/writing-tests/visual-testing">visual</a> <em>and/or</em> <a target="_blank" href="https://storybook.js.org/docs/writing-tests/snapshot-testing/snapshot-testing">snapshot testing</a> combined with <em>interactive documentation</em>, and probably many more that I don't even know about.</p>
<p>However, these tools are well documented and have huge communities around them, and to be completely honest - I don't think any of these tests are <em>hard</em> (except maybe the <em>contract tests</em>).</p>
<p>When I started working on mobile test automation, I quickly realised that this was not the case. The more I got into mobile software development, the more confusing it became, the mere amount of dependencies and requirements to just get the application running was really bizarre, let alone automating tests on it. The documentation on the various technologies was sometimes not very helpful, and there were no articles on the full spectrum of possible mobile environments.</p>
<p>So let's fix that and talk about mobile test automation, shall we?</p>
<h2 id="heading-development-frameworks-overview">Development Frameworks Overview</h2>
<p>First, before we go any further, we need to consider what technologies will be used to build the applications that will be tested with the mobile testing frameworks. The popular frameworks for mobile development are:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Name</strong></td><td><strong>Maintainer</strong></td><td><strong>Technology</strong></td><td><strong>Supported Platforms</strong></td><td><strong>Product Page</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong><em>Flutter</em></strong></td><td>Google</td><td>Dart</td><td>Android / iOS / Web / Windows / macOS</td><td><a target="_blank" href="https://flutter.dev/">https://flutter.dev/</a></td></tr>
<tr>
<td><strong><em>React Native</em></strong></td><td>Meta</td><td>JavaScript</td><td>Android / iOS / Web</td><td><a target="_blank" href="https://reactnative.dev/">https://reactnative.dev/</a></td></tr>
<tr>
<td><strong><em>Ionic</em></strong></td><td>Ionic</td><td>JavaScript</td><td>Android / iOS / Web / Windows / macOS</td><td><a target="_blank" href="https://ionic.io/">https://ionic.io/</a></td></tr>
<tr>
<td><strong><em>NativeScript</em></strong></td><td>OpenJS Foundation</td><td>JavaScript</td><td>Android / iOS / Web</td><td><a target="_blank" href="https://nativescript.org/">https://nativescript.org/</a></td></tr>
<tr>
<td><strong><em>.NET MAUI</em></strong></td><td>Microsoft</td><td>C#</td><td>Android / iOS / Windows / macOS</td><td><a target="_blank" href="https://dotnet.microsoft.com/en-us/apps/maui">https://dotnet.microsoft.com/en-us/apps/maui</a></td></tr>
<tr>
<td><strong><em>Cordova</em></strong></td><td>Apache Software Foundation</td><td>JavaScript</td><td>Android / iOS / Web / Windows / macOS</td><td><a target="_blank" href="https://cordova.apache.org/">https://cordova.apache.org/</a></td></tr>
<tr>
<td><strong><em>Xcode</em></strong></td><td>Apple</td><td>Swift</td><td>iOS</td><td><a target="_blank" href="https://developer.apple.com/xcode/">https://developer.apple.com/xcode/</a></td></tr>
<tr>
<td><strong><em>Android Studio</em></strong></td><td>Google</td><td>Kotlin</td><td>Android</td><td><a target="_blank" href="https://developer.android.com/studio">https://developer.android.com/studio</a></td></tr>
</tbody>
</table>
</div><p>According to the <a target="_blank" href="https://survey.stackoverflow.co/2024/technology#1-other-frameworks-and-libraries">2024 Stack Overflow survey</a>, <strong><em>Flutter</em></strong> is the most popular framework for cross-platform development. It's followed by <strong><em>React Native</em></strong>, <strong><em>Electron</em></strong> (not listed in our table because it only supports desktop app development), <strong><em>.NET MAUI</em></strong> (and it's older brother <strong><em>Xamarin</em></strong>, which has been replaced by <strong><em>MAUI</em></strong>), <strong><em>Ionic</em></strong>, and <strong><em>Cordova</em></strong> closing the list. Interestingly, there is no mention of <strong><em>NativeScript</em></strong> at all.</p>
<h2 id="heading-mobile-test-automation-frameworks-overview"><strong>Mobile Test Automation Frameworks Overview</strong></h2>
<p>Below is a general overview of the <em>test automation frameworks</em>. There are some other frameworks that I have intentionally left out because they do not seem to be very popular (e.g. <a target="_blank" href="https://github.com/google/EarlGrey/tree/earlgrey2"><strong>EarlGray</strong></a>, which is Google's framework for iOS automation), and they are most likely wrappers of the technologies listed in the table anyway (e.g. <strong>Selendroid</strong> or any of the cloud mobile test automation providers).</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Name</strong></td><td><strong>Maintainer</strong></td><td><strong>Main Feature(s)</strong></td><td><strong>Supported Native Platforms</strong></td><td><strong>Product Page</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong><em>Espresso</em></strong></td><td>Google</td><td>Google’s default testing framework</td><td>Android</td><td><a target="_blank" href="https://developer.android.com/training/testing/espresso">https://developer.android.com/training/testing/espresso</a></td></tr>
<tr>
<td><strong><em>XCUITest</em></strong></td><td>Apple</td><td>Apple’s default testing framework</td><td>iOS / macOS</td><td><a target="_blank" href="https://developer.apple.com/documentation/xctest/user_interface_tests">https://developer.apple.com/documentation/xctest/user_interface_tests</a></td></tr>
<tr>
<td><strong><em>Appium</em></strong></td><td>OpenJS Foundation/ Open-source (<a target="_blank" href="https://github.com/appium/appium?tab=Apache-2.0-1-ov-file#Apache-2.0-1-ov-file">Apache-2.0 license</a>).</td><td><a target="_blank" href="https://github.com/appium/appium?tab=Apache-2.0-1-ov-file#Apache-2.0-1-ov-file">Supports all</a> of the mainstream platforms</td><td>Android, iOS, macOS, Windows, Roku, tvOS, Android TV</td><td><a target="_blank" href="https://appium.io/docs/en/latest/">https://appium.io/docs/en/latest/</a></td></tr>
<tr>
<td><strong><em>Detox</em></strong></td><td>Wix.com / Open-source (<a target="_blank" href="https://github.com/wix/Detox?tab=MIT-1-ov-file#readme">MIT</a>)</td><td>Tightly coupled with the React Native architecture</td><td>Android, iOS</td><td><a target="_blank" href="https://wix.github.io/Detox/">https://wix.github.io/Detox/</a></td></tr>
<tr>
<td><strong><em>Maestro</em></strong></td><td>Mobile.dev <strong>/</strong> Open-source (<a target="_blank" href="https://github.com/mobile-dev-inc/maestro?tab=Apache-2.0-1-ov-file#readme">Apache 2.0 License</a>)</td><td>Due to implementation of <code>*.yaml</code> files as its test files format, it’s probably the most effortless mobile test automation framework</td><td>Android, iOS</td><td><a target="_blank" href="https://maestro.mobile.dev/">https://maestro.mobile.dev/</a></td></tr>
</tbody>
</table>
</div><h3 id="heading-appium-based-frameworks-overview"><strong>Appium-based Frameworks Overview</strong></h3>
<p>From the above list, <strong>Appium</strong> can be considered as the most comprehensive and therefore the most complicated framework of them all. In short, it consists of four basic elements: <strong>Appium Core</strong>, <strong>Drivers</strong>, <strong>Clients</strong> and <strong>Plugins</strong>.</p>
<p>We won't go into the details of what each element stands for, but to give you a better understanding of what technologies are supported by the <strong>Appium</strong> framework, here is a list of so-called <strong>Clients</strong>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Name</strong></td><td><strong>Technology</strong></td><td><strong>Product Page</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong><em>WebdriverIO</em></strong></td><td>JavaScript (Node.js)</td><td><a target="_blank" href="https://webdriver.io/">https://webdriver.io/</a></td></tr>
<tr>
<td><strong><em>Appium Python Client</em></strong></td><td>Python</td><td><a target="_blank" href="https://github.com/appium/python-client">https://github.com/appium/python-client</a></td></tr>
<tr>
<td><strong><em>Appium Java Client</em></strong></td><td>Java</td><td><a target="_blank" href="https://github.com/appium/java-client">https://github.com/appium/java-client</a></td></tr>
<tr>
<td><strong><em>AppiumLib</em></strong></td><td>Ruby</td><td><a target="_blank" href="https://github.com/appium/ruby_lib">https://github.com/appium/ruby_lib</a></td></tr>
<tr>
<td><strong><em>Appium .NET Client</em></strong></td><td>C#</td><td><a target="_blank" href="https://github.com/appium/dotnet-client/">https://github.com/appium/dotnet-client/</a></td></tr>
</tbody>
</table>
</div><h2 id="heading-scenario-based-guide-to-selecting-mobile-testing-frameworks"><strong>Scenario-Based Guide to Selecting Mobile Testing Frameworks</strong></h2>
<p>When you would have to make one of the key decisions in the context of testing strategy, that is, the choice of a framework for automating mobile end-to-end testing the choice is quite complicated, as it depends on many factors.</p>
<p>Let's do a little thought experiment and try to describe some possible scenarios to help you decide.</p>
<p><strong><em>TL;DR:</em></strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735644905798/aa9f459f-cd4f-4a6e-b349-0007b1ee027a.jpeg" alt class="image--center mx-auto" /></p>
<ul>
<li><p>When developers are using native development tools and want to maintain grey/white box end-to-end testing and/or you’re building an MVP: <strong>Espresso (for Android)</strong> / <strong>XCUITest (for iOS)</strong></p>
</li>
<li><p>If you're developing an MVP and you only need UI testing (<a target="_blank" href="https://maestro.mobile.dev/getting-started/installing-maestro#connecting-to-your-device">without physical iOS devices</a> and with <a target="_blank" href="https://maestro.mobile.dev/advanced/javascript">limited scripting possibilites</a>): <strong>Maestro</strong></p>
</li>
<li><p>If you're developing a cross-platform <strong>React Native</strong> application and want to improve test scripting experience with <a target="_blank" href="https://wix.github.io/Detox/docs/copilot/testing-with-copilot">Large Language Models</a>: <strong>Detox</strong></p>
</li>
<li><p>For long-term projects that require stable solutions: <strong>Appium-based</strong> platforms</p>
</li>
</ul>
<h3 id="heading-scenario-1-android-or-ios-only"><strong>Scenario 1: Android or iOS Only</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735645790059/8b1b2e17-7ab7-4e5b-accc-ae894285d679.jpeg" alt class="image--center mx-auto" /></p>
<p>The first scenario is an example for likely early stages of development (e.g. seed stage of a startup) when the focus is on only one mobile platform. This means that the natural choice should be either <strong>Espresso</strong> (for Android) or <strong>XCUITest</strong> (for iOS).</p>
<p>However, there may be other things to consider, such as:</p>
<ol>
<li><strong>Resource-Constrained Environments:</strong></li>
</ol>
<p>For example, if the application is being developed on a tight schedule and/or budget, and the developers simply do not have the time to maintain the end-to-end test suites, choosing complex <strong>Espresso</strong> or <strong>XCUITest</strong> frameworks can end up being a disaster.</p>
<p>In this type of environment, I would suggest using <strong>Maestro</strong> as it seems to be the most hassle-free and does not require too much time and effort to maintain test suites and configuration.</p>
<ol start="2">
<li><strong>Future Proofing for Long-Term Projects:</strong></li>
</ol>
<p>If your project's time and budget constraints are not the things that keep you up at night, then you should probably think a little more about the potential development path.</p>
<p>I want to emphasise that we should <strong><em>avoid over-engineering</em></strong>, but on the other hand, it's also important to choose a framework that we can use in a more mature test environment. By a more mature test environment I mean things like <em>automated test data generation and cleanup</em> (which can be achieved with <a target="_blank" href="https://engineering.cloudflight.io/test-automation-api-based-model">API-based model</a>) or implementation into the existing <em>CI/CD pipelines</em>. And of course we want these things to be relatively easy to implement.</p>
<p>In summary, if you are sure that your project is entirely <strong>Android</strong> or <strong>iOS</strong> based, developers are the ones who will write and maintain the test suites, and you do not plan to extend it to the opposite technology, I would suggest using <strong>Espresso</strong> or <strong>XCUITest</strong>, as they will give you the most solutions adapted to your existing codebase. Otherwise, I'd lean toward the <strong>Appium-based</strong> solutions because they're much more extensible and can be used in black-box environments.</p>
<h3 id="heading-scenario-2-android-and-ios"><strong>Scenario 2: Android and iOS</strong></h3>
<p>Most of the time, companies want to target the broader audience, and since <strong>Android</strong> has <strong><em>~72%*</em></strong> of the market share, which means that <strong>iOS</strong> users have <strong><em>~28%*</em></strong> of the market share, it means that most companies want to target both platforms.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>*Data as of 31/12/2024 from </em><a target="_self" href="https://gs.statcounter.com/os-market-share/mobile/worldwide"><em>https://gs.statcounter.com/os-market-share/mobile/worldwide</em></a></div>
</div>

<p>This scenario, at least from my perspective, is much more complex in terms of things that need to be considered before an informed decision can be made.</p>
<ol>
<li><p><strong>Separate development teams for Android and iOS</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735646158995/95c49be3-5345-4c77-9e7e-8f5aeec678c4.jpeg" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>If your application is developed by separate teams using <strong>native development tools</strong> (i.e. <strong>Android Studio</strong> and/or <strong>Xcode</strong>), you can probably integrate <strong>Espresso</strong> and <strong>XCUITest</strong> to perform end-to-end testing. However, I would like to point out that this may be a subpar solution, as other frameworks allow you to test both platforms using the same test scripts.</p>
<p>So, for the situation mentioned in this sub-scenario, I'd suggest using <strong>Espresso</strong> and <strong>XCUITest</strong> for smaller scale tests (<em>component testing</em> if you will) that would be maintained by the developers themselves and integrating broader scoped frameworks for end-to-end testing (i.e. <strong>Appium</strong> or <strong>Maestro</strong>).</p>
<ol start="2">
<li><p><strong>Cross-platform development</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735646686785/3264fb84-651b-422a-84b8-292903597ed6.jpeg" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>As you can see in the table mentioned in the <strong>Development Frameworks Overview</strong> section, there are many possible frameworks to choose from to develop cross-platform applications. Most frameworks support all platforms (mobile, web, and desktop), but when it comes to mobile development, we focus mostly on <strong>Android</strong> and <strong>iOS</strong>.</p>
<p>Unfortunately, in order to figure out which framework we should choose to write and maintain end-to-end mobile tests, we need to proceed with the sub-categorisation of possible needs and settings.</p>
<ul>
<li><p>If you really don't have the time to maintain test suites, and therefore need something simple, almost effortless to write and maintain, I would suggest going with the <strong>Maestro</strong>.</p>
</li>
<li><p>If you are an <strong>SDET</strong> or an <strong>experienced test automation engineer</strong>, you may want to lean more towards <strong>Appium-based</strong> tools, as <strong>Appium</strong> gives you all the benefits of mobile test automation as well as the freedom to configure the entire test automation framework yourself. I'm not saying that <strong>Maestro</strong> can't do this, but please take a look at the <strong>Maestro Afterthoughts</strong> for more insight.</p>
</li>
</ul>
<ol start="3">
<li><p><strong>Cross-platform development with React Native</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735647313722/f77f7473-4f1c-443e-8dbe-c7e1fd88f245.jpeg" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>The tool I haven't mentioned yet is <strong>Detox</strong>. This is because <strong>Detox</strong> is specifically designed for grey-box end-to-end testing for <strong>React Native</strong>. I'd recommend giving it a try if you're developing a <strong>React Native</strong> application, especially given the fact that it's the first known framework to incorporate the <a target="_blank" href="https://wix.github.io/Detox/docs/copilot/testing-with-copilot">Large Language Models</a> to write tests in natural language.</p>
<p>However, if you and your team are developing applications for external customers (so-called <em>enterprise-grade applications</em>) that require stable tools, I'd strongly recommend using <strong>Appium-based</strong> tools because they are much better documented and have much larger communities built around them.</p>
<h3 id="heading-scenario-3-android-ios-web-and-more"><strong>Scenario 3: Android, iOS Web and More</strong></h3>
<p>The last scenario we'll talk about is native mobile development combined with any other version of the app. This is paradoxically the easiest of all.</p>
<p><strong>Appium</strong>.</p>
<p>There is simply no other solution that would allow you to test on all of the available platforms.</p>
<p>Of course, if you'd like, you can mix and match <strong>Maestro</strong> for mobile testing and <strong>Playwright</strong> for web testing. Or <strong>Detox</strong> for mobile, <strong>Appium</strong> for smart TV, and <strong>Selenium</strong> for web, if that's what you want. It just doesn't seem like a good idea, especially since some of the <strong>Appium-based</strong> frameworks allow you to create a single test script that would run sequentially on multiple platforms.</p>
<p>For example, <strong>WebdriverIO</strong> allows you to do something on one mobile device, then switch to a separate web application to change/verify values and perform some other actions on another mobile device.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735648112651/fc0ceb5e-601d-4f59-8957-aba039b30cd7.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-maestro-afterthoughts">Maestro Afterthoughts</h2>
<p>I felt that <strong>Maestro</strong> might seem like a great solution in the end, which it can be in some cases, but I also want you to think about possible downsides if you decide to go with it.</p>
<p><strong>Maestro</strong> gives you the ability to configure the test automation framework (e.g. <a target="_blank" href="https://maestro.mobile.dev/advanced/javascript">run JavaScript code</a>), but since it's written with a specific architecture in mind, it's more of an add-on to the standard <strong>YAML</strong> flow files than anything else.</p>
<p>Another thing to keep in mind when talking about the <strong>Maestro</strong> is last year's <a target="_blank" href="https://www.reddit.com/r/softwaretesting/comments/17wqis1/why_is_noone_talking_about_this_cypress_blocking/">Cypress intellectual property update case</a>. In a nutshell, the <strong>Cypress</strong> team <a target="_blank" href="https://www.cypress.io/blog/update-defense-intellectual-property">removed the ability to use third-party services for dashboards</a> that were previously free, essentially forcing companies to use their own cloud platform.</p>
<p>Not only is that platform not free, but it also prevents <strong>Cypress</strong> from being used where contracts with customers specifically block the use of cloud platforms due to GDPR or similar laws. I mention this because <strong>Maestro</strong> has its own cloud platform for running tests called <strong>Robin</strong>, and it's possible that the similar scenario to the <strong>Cypress</strong> update could happen to <strong>Maestro</strong> as well.</p>
<h2 id="heading-development-for-the-apple-ecosystem">Development for the Apple ecosystem</h2>
<p>The last thing I wanted to mention here is that if you want to write tests for the <strong>Apple</strong> ecosystem, namely <strong>iOS</strong>, <strong>iPadOS</strong>, <strong>macOS</strong>, or <strong>tvOS</strong>, you actually need to own a machine based on <strong>macOS</strong> (either a MacBook, Mac Mini/Studio/Pro, or iMac) because you need to have <strong>Xcode</strong> installed.</p>
<p>The reason is simple - it contains the <strong>Simulator</strong> application, which allows you to run your application in the virtual environment and is required to run tests on <strong>real devices</strong>. You'll also need an <a target="_blank" href="https://developer.apple.com/support/compare-memberships/">Apple Account</a>, and if you want to publish your application on the <strong>AppStore</strong>, you'll need to sign up for the paid <a target="_blank" href="https://developer.apple.com/support/compare-memberships/">Apple Developer Program</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Quantized YOLO for Edge Solutions]]></title><description><![CDATA[In the previous article, we discussed how we set up pigeon detection on an edge device. We took for granted the existence of a quantized model that can be deployed. This is not a straightforward method, let's discuss further in this article how to ac...]]></description><link>https://engineering.cloudflight.io/quantized-yolo-for-edge-solutions</link><guid isPermaLink="true">https://engineering.cloudflight.io/quantized-yolo-for-edge-solutions</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[Computer Vision]]></category><category><![CDATA[quantization]]></category><category><![CDATA[edgecomputing]]></category><dc:creator><![CDATA[AdamP]]></dc:creator><pubDate>Tue, 16 Apr 2024 13:42:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710950569571/ee7af826-54e5-4c32-a326-985179595af6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous article, we discussed how we set up pigeon detection on an edge device. We took for granted the existence of a quantized model that can be deployed. This is not a straightforward method, let's discuss further in this article how to achieve deployable models. If you have not read the previous article, please read it here: <a target="_blank" href="https://engineering.cloudflight.io/pigeons-on-the-edge">Pigeons On the Edge</a></p>
<h2 id="heading-quantization">Quantization</h2>
<p>Quantization is the process of decreasing the computational precision of a model to a lower datatype, e.g. FP32 (floating-point) to INT8. It is strongly dependent on the hardware, which datatype you should prefer while quantizing. In this article, we will mainly focus on INT8 for the following reasons:</p>
<ul>
<li><p>Coral Ai Edge TPU only supports INT8 operations</p>
</li>
<li><p>NXP iMX8 Plus is more efficient in INT8 precision and consumes less power</p>
</li>
<li><p>Newer Intel CPUs have dedicated INT8 co-processors and instruction sets i.e. Intel® Advanced Matrix Extensions, which can do 2048 INT8 operations per cycle</p>
</li>
</ul>
<p>What do we expect from a quantized model on dedicated hardware?</p>
<ul>
<li><p>Efficient execution in both performance and power consumption</p>
</li>
<li><p>Decreased model size:</p>
<ul>
<li><p>Coral Ai Edge TPU has only 8MB of cache</p>
</li>
<li><p>Newer Large Language Models are even quantized to INT4 to be able to fit into consumer GPU hardware</p>
</li>
</ul>
</li>
<li><p>Decreased model accuracy, due to lower computation precision</p>
</li>
</ul>
<p>How to quantize a model?</p>
<ul>
<li><p>Post-training quantization, where an already trained model is quantized. Each edge device normally has its quantizer to deploy models.</p>
</li>
<li><p>Quantization aware training, where during training not only the model accuracy but also the quantization factors are optimized, which increases the complexity of training and deployment. If necessary, activation functions can also be modified according to hardware requirements.</p>
</li>
</ul>
<p>How does quantization work?</p>
<ul>
<li><p>Scaled Integers, where real numbers are represented as integers multiplied by a factor and shifted with a bias</p>
</li>
<li><p>Lookup table (LUT), complex functions like sigmoid and softmax can be precomputed and their outputs can be stored</p>
</li>
<li><p>When these methods are applied a representative dataset is required</p>
</li>
<li><p>There exist other techniques, but these are the most common to be applied</p>
</li>
</ul>
<h2 id="heading-deploying-yolo">Deploying YOLO</h2>
<p>Now that we have a better overview of quantization let's try to quantize the YOLO model. To continue, we need to have a high-level understanding of the YOLO model:</p>
<ul>
<li><p>Backbone, which is a convolutional neural network and outputs a feature pyramid</p>
</li>
<li><p>Head #1, where the last 3 layers of the pyramid are upscaled and convolved</p>
</li>
<li><p>Head #2, where the Detection layer is executed</p>
</li>
<li><p>Head #3, where the scores and bounding box coordinates are computed</p>
</li>
</ul>
<h3 id="heading-fixing-the-head-3-of-yolo">Fixing the Head #3 of YOLO</h3>
<p>When applying INT8 quantization on the YOLO7 model, the output values were corrupted. While analyzing the code, it was observed that pixel coordinates and the class score values are not in the same range, therefore quantization collapses. After applying normalization on the coordinate values by image size, a more reasonable output was visible, but still, some values were wrong. Models with large input image sizes were still suffering from quantization collapse. After analyzing the model quantization parameters (factor, bias) it was found that a large numerical instability is present. This was further improved by using normalized scaling factors instead of normalizing output values by the image size. Precomputing the scaling factors is possible because we quantize static models, meaning the input size must be always the same and cannot change during inference.</p>
<p>As the next step, YOLO8 was tested, where the same numerical instability was visible. Changing here to use also normalized scaling factors instead of normalizing by image size solved a bug and it further improved the model precision by 4% compared to the previously reported values.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711458746403/0dd21843-3207-4772-bb83-107c4b53fa78.png" alt class="image--center mx-auto" /></p>
<p>We can observe this on the first image, where normalizing with image size results in a factor difference of 10^5.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711458646254/85e188a0-4418-4ac4-ae6d-195784f49d43.png" alt class="image--center mx-auto" /></p>
<p>Where on the second image we see that using normalized scaling factors for bounding box calculation our quantization factor difference is only 10^2.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Model</strong></td><td><strong>mAP50-95</strong></td><td><strong>Note</strong></td></tr>
</thead>
<tbody>
<tr>
<td>yolov8n FP32</td><td>37.3</td><td>Unquantized</td></tr>
<tr>
<td>yolov8n INT8 Main</td><td>8.1</td><td>Bug in repo</td></tr>
<tr>
<td>yolov8n INT8 Claimed</td><td>28.7</td><td>Claimed results</td></tr>
<tr>
<td><strong>yolov8n INT8 Fixed</strong></td><td><strong>32.9</strong></td><td><strong>Fixed results</strong></td></tr>
</tbody>
</table>
</div><h3 id="heading-should-quantization-be-applied-to-head-3-at-all">Should quantization be applied to Head #3 at all?</h3>
<p>Tests have shown that if the last mathematical operations are excluded from the quantization the precision of the model increases, while the processing time is barely increasing. On a device with only INT8 operations, this would mean that the final steps need to be executed on the CPU, if FP32 instructions are available. This would also mean that normalization is not necessary and the previous discussion would be irrelevant. Detaching the head is at the moment rather cumbersome to implement for TFLite quantization (TPU, NXP) and only can be achieved with dirty hacks.</p>
<p>For Intel CPUs, this is a different scenario as operations can fall back on the CPU to be executed in FP32 precision. OpenVINO quantizer supports such changes by explicitly defining which operations should be excluded from the quantization process. Due to this freedom, normalizing is not necessary as the quantizer implicitly excludes operations if quantization collapses. After careful testing to exclude the whole Head #3, the precision of the model has been improved by 1.4% while the average inference speed only increased by 0.3% on an Intel 9th Gen CPU.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Model</strong></td><td><strong>mAP50-95</strong></td><td>Inference</td><td><strong>Note</strong></td></tr>
</thead>
<tbody>
<tr>
<td>yolov8n FP32</td><td>37.3</td><td>PyTorch</td><td>Unquantized</td></tr>
<tr>
<td>yolov8n INT8</td><td>32.9</td><td>TFLite (TPU, NXP)</td><td>Fixed results</td></tr>
<tr>
<td><strong>yolov8n INT8 + FP32</strong></td><td><strong>35.2</strong></td><td><strong>TFLite (TPU, NXP)</strong></td><td><strong>Detached Head #3</strong></td></tr>
<tr>
<td>yolov8n INT8</td><td>35.7</td><td>OpenVINO (Intel)</td><td>Main branch</td></tr>
<tr>
<td><strong>yolov8n INT8 + FP32</strong></td><td><strong>37.1</strong></td><td><strong>OpenVINO (Intel)</strong></td><td><strong>Improved results</strong></td></tr>
</tbody>
</table>
</div><p>NOTE: OpenVINO applies per-channel quantization, while TFLite can be switched between per-tensor or per-channel. Per-tensor has one factor and bias, while per-channel has for each channel a factor and bias.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Model</strong></td><td><strong>mAP50-95</strong></td><td>Inference</td><td><strong>Note</strong></td></tr>
</thead>
<tbody>
<tr>
<td>yolov8n INT8</td><td>32.9</td><td>TFLite (TPU, NXP)</td><td>per-tensor</td></tr>
<tr>
<td>yolov8n INT8</td><td>33.9</td><td>TFLite (TPU, NXP)</td><td>per-channel</td></tr>
<tr>
<td>yolov8n INT8 + FP32</td><td>35.2</td><td>TFLite (TPU, NXP)</td><td>per-tensor</td></tr>
<tr>
<td>yolov8n INT8 + FP32</td><td>36.3</td><td>TFLite (TPU, NXP)</td><td>per-channel</td></tr>
</tbody>
</table>
</div><h3 id="heading-activation-functions-on-edge-devices">Activation Functions on Edge Devices</h3>
<p>Since some of the selected devices have restricted instruction sets, different activation functions are needed for deployed models.</p>
<ul>
<li><p>LeakyReLU</p>
<ul>
<li><p>YOLOv7-Tiny is trained with LeakyReLU</p>
</li>
<li><p>Coral Ai Edge TPU does not support LeakyReLU</p>
</li>
<li><p>NXP output is corrupt using LeakyReLU</p>
</li>
<li><p>Intel works with LeakyReLU</p>
</li>
</ul>
</li>
<li><p>SiLU</p>
<ul>
<li><p>YOLOv8n is trained with SiLU</p>
</li>
<li><p>Coral Ai Edge TPU crash using SiLU</p>
</li>
<li><p>Intel works with SiLU</p>
</li>
</ul>
</li>
<li><p>ReLU6</p>
<ul>
<li><p>ReLU6 achieves lower mAP after training for both YOLO7 and YOLO8</p>
</li>
<li><p>ReLU6 has less accuracy drop after quantization</p>
</li>
<li><p>ReLU6 works both on Coral Ede TPU and NXP</p>
</li>
</ul>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Model</td><td>mAP50-95 SiLU</td><td>mAP50-95 ReLU6</td></tr>
</thead>
<tbody>
<tr>
<td>yolov8n F32</td><td>37.4</td><td>34.0</td></tr>
<tr>
<td>yolov8n INT8</td><td>33.9</td><td>31.4</td></tr>
<tr>
<td>yolov8n INT8 + FP32</td><td>36.3</td><td>33.9</td></tr>
</tbody>
</table>
</div><h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Quantization is a hardware-dependent task. To achieve the best results, one should understand both the AI model and the hardware to be deployed. The changes about <a target="_blank" href="https://github.com/ultralytics/ultralytics/pull/7372">TFLite</a> and <a target="_blank" href="https://github.com/ultralytics/ultralytics/pull/7516">OpenVINO</a> mentioned in this article regarding YOLO8 have been merged into the main repository.</p>
]]></content:encoded></item><item><title><![CDATA[Pigeons on the Edge]]></title><description><![CDATA[In this article, we will talk about deploying state-of-the-art computer vision object detection on low-power and low-cost edge devices. Finally, we see a budget hardware setup, which can detect pigeons in almost real-time.
State of the AI
In recent y...]]></description><link>https://engineering.cloudflight.io/pigeons-on-the-edge</link><guid isPermaLink="true">https://engineering.cloudflight.io/pigeons-on-the-edge</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[AI]]></category><category><![CDATA[Raspberry Pi]]></category><category><![CDATA[Computer Vision]]></category><dc:creator><![CDATA[AdamP]]></dc:creator><pubDate>Mon, 15 Apr 2024 22:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710951978563/f73708f7-e045-4a1a-830c-b6cf5e6e2791.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, we will talk about deploying state-of-the-art computer vision object detection on low-power and low-cost edge devices. Finally, we see a budget hardware setup, which can detect pigeons in almost real-time.</p>
<h2 id="heading-state-of-the-ai">State of the AI</h2>
<p>In recent years, there has been a huge advance in AI with the introduction of Large Language Models. These methods have also been adopted by the Computer Vision community, where images are converted to tokens and passed to a Language Model for classification, detection, or other tasks, these are called Vision Language Models. Although these methods show better performance in regards to accuracy or precision, they require far more processing power compared to their older relatives, let's call them the Convolutional Neural Network ones. The need for processing power becomes an even larger issue when deploying a model on the edge.</p>
<ul>
<li><p>Large Language Model (LLM), e.g ChatGPT, Llama2</p>
</li>
<li><p>Vision Language Model (VLM), e.g GPT-4V, Swin-L or ViTL</p>
</li>
<li><p>Convolutional Neural Network (CNN), e.g YOLO or EfficientNet</p>
</li>
</ul>
<h2 id="heading-object-detection">Object Detection</h2>
<p>Object detection is a task in computer vision, where we take an image as an input and localize and classify objects within the input image. The de facto metric to assess the quality of an object detector is the mean Average Precision (mAP). This is calculated by the sum of the intersection between the predicted bounding box and the annotated bounding box.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Model</td><td>mAP (50-95) COCO</td><td>Model Size (#params)</td></tr>
</thead>
<tbody>
<tr>
<td>Swin-L(DINO)</td><td>63.2</td><td>218M</td></tr>
<tr>
<td>ViT-L (Co-DETR)</td><td>65.9</td><td>304M</td></tr>
<tr>
<td>YOLOv8x</td><td>53.9</td><td>68.2M</td></tr>
<tr>
<td>YOLOv8s</td><td>44.9</td><td>11.2M</td></tr>
<tr>
<td>YOLOv8n</td><td>37.3</td><td>3.2M</td></tr>
</tbody>
</table>
</div><h3 id="heading-yolo-ultralytics-on-github">YOLO (Ultralytics on GitHub)</h3>
<p>YOLO (You Only Look Once) is the state-of-the-art convolutional neural network based method for object detection, but the newer YOLO versions can perform other tasks like segmentation, pose estimation, or classification.</p>
<p><img src="https://raw.githubusercontent.com/ultralytics/assets/main/im/banner-tasks.png" alt /></p>
<p>As we see from the table above VLM methods outperform CNN methods, but require a higher number of trainable parameters. The number of parameters is an indication of both computational power and memory usage, but multiple factors influence inference speed. YOLO comes also in different sizes, indicated by the last character in the name. The smaller a model is, the faster the inference is, and the smaller the memory consumption is, but also the accuracy decreases.</p>
<p><img src="https://raw.githubusercontent.com/ultralytics/assets/main/yolov8/yolo-comparison-plots.png" alt="YOLOv8 performance plots" /></p>
<h2 id="heading-edge-devices">Edge Devices</h2>
<p>Edge devices are limited by processing capabilities and power consumption. They give AI capabilities for IoT devices, sensors, cameras, drones, and smartphones. There are low-end solutions that can cost 20$, but one can also choose from high-end System-on-Chip (SoC) solutions above 1000$. The rule of thumb is if it costs more then it has higher processing power and more power consumption. When choosing the hardware, there will be a tradeoff between price and performance. The following tables show a few examples of devices as of February 2024.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><em>Low-End Edge</em></td><td>Coral AI</td><td>NXP iMX8 Plus</td><td>Hailo8</td></tr>
</thead>
<tbody>
<tr>
<td>Type</td><td>Dedicated Chip (NPU)</td><td>SoC (arm64) + GPU + NPU</td><td>Dedicated Chip (NPU)</td></tr>
<tr>
<td>Instruction Set SoC</td><td>N/A</td><td>FP,INT</td><td>N/A</td></tr>
<tr>
<td>Instruction Set GPU</td><td>N/A</td><td>FP32</td><td>N/A</td></tr>
<tr>
<td>Instruction Set NPU</td><td>INT8</td><td>INT8</td><td>INT8</td></tr>
<tr>
<td>Form</td><td>USB, PCIe, m.2, Chip</td><td>Board, Chip</td><td>PCIe, m.2, Chip</td></tr>
<tr>
<td>TOPS (INT)</td><td>4 TOPS</td><td>2.3 TOPS</td><td>26 TOPS</td></tr>
<tr>
<td>FLOPS (FP)</td><td>N/A</td><td>7.2 GFLOPS</td><td>N/A</td></tr>
<tr>
<td>Power Usage</td><td>~2 Watt</td><td>~5-15 Watt</td><td>~2.5 Watt</td></tr>
<tr>
<td>Price</td><td>~20 $</td><td>~60 $</td><td>~140 $</td></tr>
</tbody>
</table>
</div><div class="hn-table">
<table>
<thead>
<tr>
<td><em>High-End Edge</em></td><td>Intel Ultra</td><td>Qualcomm Snapdragon v3</td><td>Nvidia Jetson Orin</td></tr>
</thead>
<tbody>
<tr>
<td>Type</td><td>SoC (x64) + GPU + NPU</td><td>SoC (arm64) + GPU + NPU</td><td>SoC (arm64) + GPU + NPU</td></tr>
<tr>
<td>Instruction Set SoC</td><td>FP,INT,BF16</td><td>TBA</td><td>FP,INT</td></tr>
<tr>
<td>Instruction Set GPU</td><td>F16</td><td>TBA</td><td>FP32, FP16</td></tr>
<tr>
<td>Instruction Set NPU</td><td>INT8</td><td>INT4, TBA</td><td>INT8</td></tr>
<tr>
<td>Form</td><td>Chip</td><td>Chip</td><td>Board, Chip</td></tr>
<tr>
<td>TOPS (INT)</td><td>max 34 TOPS</td><td>TBA 75 TOPS</td><td>20-275 TOPS</td></tr>
<tr>
<td>FLOPS (FP)</td><td>max 4.5 TFLOPS</td><td>TBA</td><td>max 5.3 TFLOPS</td></tr>
<tr>
<td>Power Usage</td><td>~ 18-64 Watt</td><td>TBA</td><td>~7-60 Watt</td></tr>
<tr>
<td>Price</td><td>~ 375 $</td><td>~ 900 $ TBA</td><td>~400-1100 $</td></tr>
</tbody>
</table>
</div><h3 id="heading-deploying-a-quantized-yolo">Deploying a quantized YOLO</h3>
<p>In this post, we assume that we already have a quantized model. In short, quantization is the step where the model precision is decreased, for example instead of using FP32, we quantize to INT8. While on many hardware this comes as an improvement in inference speed, the accuracy of the detection will decrease. We discuss quantization in more detail in the next chapter, <a target="_blank" href="https://engineering.cloudflight.io/quantized-yolo-for-edge-solutions">Quantized YOLO for Edge Solutions</a>.</p>
<p>Model deployment on edge hardware is different for each device. Let's discuss a few cases:</p>
<ul>
<li><p>Coral AI works only with INT8 precision. This means the model weights are in INT8 precision and inference is performed as integers.</p>
</li>
<li><p>NXP is most efficient also using only INT8 precision.</p>
</li>
<li><p>Intel can execute inference in different ways. It also depends on, which generation CPU is being used. In general, it consists of a CPU where you can execute either in FP32 or combine FP32 with INT8, an iGPU where you can execute in FP16, and an NPU where you can execute only in INT8.</p>
</li>
</ul>
<p>Now let's test the inference speed on the following hardware:</p>
<ul>
<li><p>*Intel i7-9750H</p>
</li>
<li><p>**Raspberry Pi4 + Coral AI Edge TPU</p>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Model</td><td>mAP50-95</td><td>Inference</td><td>Avg Speed</td></tr>
</thead>
<tbody>
<tr>
<td>yolov8n F32</td><td>37.4</td><td>Unquantized, Intel*</td><td>24.40 ms ~ 40 FPS</td></tr>
<tr>
<td>yolov8n INT8+FP32</td><td>37.1</td><td>Quantized, Intel*</td><td>15.18 ms ~ 66 FPS</td></tr>
<tr>
<td>yolov8n FULL INT8</td><td>32.9</td><td>Quantized, Coral**</td><td>61.00 ms ~ 16 FPS</td></tr>
</tbody>
</table>
</div><h2 id="heading-the-setup">The Setup</h2>
<p>This is a home-made setup, which can detect pigeons in almost real-time. This is a Raspberry Pi4 connected with a battery pack and 2 Coral Ai Edge TPUs. One for bird detection and the other for bird classification. After deploying this setup, together with two plastic crows, no more pigeons landed on my balcony in the last 1.5 years. Check out my repo to make sure no pigeons make your balcony dirty. <a target="_blank" href="https://github.com/adamp87/pigeon">GitHub Repository</a></p>
<p><a target="_blank" href="https://github.com/adamp87/pigeon"><img src="https://github.com/adamp87/pigeon/raw/main/doc/hardware.jpg" alt class="image--center mx-auto" /></a></p>
<p><img src="https://github.com/adamp87/pigeon/raw/main/doc/pigeons.jpg" alt="https://github.com/adamp87/pigeon" /></p>
<p>For more technical details on the quantization, continue reading the next chapter, <a target="_blank" href="https://engineering.cloudflight.io/quantized-yolo-for-edge-solutions">Quantized YOLO for Edge Solutions</a></p>
]]></content:encoded></item><item><title><![CDATA[Elevating Test Automation Excellence: Leveraging Interaction Modes in Team Topologies]]></title><description><![CDATA[In the first part How to enhance Test Automation with Team Topologies, we covered team structures in Team Topologies, with attention to Enabling teams.
Today we will focus on the interaction modes and highlight the possibilities in the context of a t...]]></description><link>https://engineering.cloudflight.io/elevating-test-automation-excellence-leveraging-interaction-modes-in-team-topologies</link><guid isPermaLink="true">https://engineering.cloudflight.io/elevating-test-automation-excellence-leveraging-interaction-modes-in-team-topologies</guid><category><![CDATA[team topologies]]></category><category><![CDATA[test-automation]]></category><dc:creator><![CDATA[Marco Hampel]]></dc:creator><pubDate>Thu, 14 Mar 2024 09:58:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/71CjSSB83Wo/upload/7ae76ec5ef453b3cbc4f5db69c84263f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the first part <a target="_blank" href="https://engineering.cloudflight.io/enhancing-test-automation-with-team-topologies-leveraging-enabling-teams">How to enhance Test Automation with Team Topologies</a>, we covered team structures in Team Topologies, with attention to Enabling teams.</p>
<p>Today we will focus on the interaction modes and highlight the possibilities in the context of a test automation enabling team.</p>
<h3 id="heading-unlocking-interaction-modes-empowering-enabling-teams">Unlocking Interaction Modes: Empowering Enabling Teams</h3>
<p>In the dynamic landscape of software development, test automation has emerged as a cornerstone for achieving speed, efficiency, and quality in product delivery. Within this ecosystem, the role of a test automation enabling team is pivotal. These teams specialize in providing the necessary expertise, tools, and support to empower other teams in implementing and maintaining robust automated testing practices. However, the success hinges not only on their technical proficiency but also on the effectiveness of their interactions with other teams.</p>
<p>In the next sections, we will explore three key interaction modes - Collaboration, X-as-a-Service, and Facilitation - and identify which modes are best suited to maximize their impact.</p>
<p><strong>Collaboration: Fostering Shared Responsibility</strong></p>
<p>Collaboration lies at the heart of effective teamwork and is particularly vital for test automation. By adopting a collaborative approach, the automation team can forge strong partnerships with development, testing, and operations teams, working together towards common goals.</p>
<ul>
<li><p><strong>Shared Understanding</strong>: Test automation enabling teams collaborate closely with other teams to gain insights into their testing needs, challenges, and priorities. By fostering open communication channels, they ensure that automated testing efforts are aligned with overall business objectives.</p>
</li>
<li><p><strong>Cross-functional Expertise</strong>: Leveraging the diverse skill sets within the organization, collaboration enables test automation enabling teams to tap into domain-specific knowledge and technical expertise. This cross-functional collaboration enriches the quality of automated tests and enhances the effectiveness of testing strategies.</p>
</li>
<li><p><strong>Continuous Improvement</strong>: Through collaborative retrospectives and feedback loops, teams can reflect on their testing practices, identify areas for improvement, and iterate on their automation strategies. By fostering a culture of continuous learning and adaptation, collaboration drives innovation and excellence in test automation.</p>
</li>
</ul>
<p><strong>X-as-a-Service: Providing Scalable Solutions</strong></p>
<p>Providing testing-related services as a centralized offering.</p>
<ul>
<li><p><strong>Standardize Practices</strong>: Through X-as-a-Service, test automation enabling teams can establish standardized testing frameworks, tools, and processes that can be leveraged across the organization. This standardization promotes consistency, reduces duplication of effort, and accelerates the adoption of automated testing practices.</p>
</li>
<li><p><strong>Scale Resources</strong>: Test automation enabling teams can scale their resources and expertise to meet the fluctuating demands of different teams and projects. By offering testing services as a scalable resource, they ensure that teams have access to the necessary support and guidance to accelerate their testing efforts.</p>
</li>
<li><p><strong>Enable Self-Service</strong>: X-as-a-Service models empower teams to become more self-sufficient in their testing endeavors. By providing self-service platforms, tools, and documentation, test automation enabling teams enable other teams to autonomously create, execute, and maintain automated tests, thereby reducing dependencies and promoting agility.</p>
</li>
</ul>
<p><strong>Facilitation: Guiding and Empowering Teams</strong></p>
<p>Facilitation plays a crucial role in guiding teams through complex challenges, fostering collaboration, and promoting innovation.</p>
<ul>
<li><p><strong>Clarify Objectives</strong>: Facilitation helps teams align on testing objectives, priorities, and strategies. By facilitating workshops, planning sessions, and brainstorming exercises, test automation enabling teams can ensure that testing efforts are focused and aligned with business goals.</p>
</li>
<li><p><strong>Resolve Conflicts</strong>: Inevitably, conflicts may arise during the testing process. Facilitation techniques such as mediation and consensus-building can help teams navigate conflicts effectively, fostering a positive and constructive working environment.</p>
</li>
<li><p><strong>Empower Teams</strong>: Facilitation empowers teams to take ownership of their testing processes and outcomes. By facilitating knowledge-sharing sessions, communities of practice, and peer learning initiatives, test automation enabling teams cultivate a culture of empowerment and collaboration.</p>
</li>
</ul>
<h3 id="heading-team-api">Team API</h3>
<p>Team API is a concept, referring to the interface or interaction points between teams within a project or an organization. Just like how software systems have APIs (Application Programming Interfaces) for communication between different software components, teams also need clear interfaces and communication channels to collaborate effectively. The Team API defines how teams interact, communicate, share information, and collaborate on work. It encompasses aspects such as responsibilities, expectations, communication channels, decision-making processes, and dependencies between teams. Establishing clear and well-defined Team APIs helps to streamline collaboration, reduce misunderstandings, and improve the overall efficiency of the organization. However, it is just as important to regularly challenge and maintain these definitions.</p>
<p>Here you can find the official <a target="_blank" href="https://github.com/TeamTopologies/Team-API-template">Team API template</a>.</p>
<h3 id="heading-our-journey-success-through-effective-interaction-modes">Our Journey <strong>- Success Through Effective Interaction Modes</strong></h3>
<p>In our journey as a test automation enabling team, embracing Team Topologies alongside Lean Coffee sessions, regular Meetups, and the introduction of a Team API has been transformative. Team Topologies provided us with a structured framework for organizing our teams and optimizing interactions, leading to improved collaboration and productivity.</p>
<p>By leveraging collaboration, X-as-a-Service, and facilitation, we can orchestrate success, driving continuous improvement, innovation, and excellence in automated testing practices. Striving to deliver high-quality software at speed, the strategic adoption of interaction modes in team topologies emerges as a critical enabler for achieving this ambitious goal.</p>
]]></content:encoded></item><item><title><![CDATA[Enhancing Test Automation with Team Topologies: Leveraging Enabling Teams]]></title><description><![CDATA[In the fast-paced world of software development, teams are constantly seeking ways to optimize their processes and deliver high-quality products efficiently. One approach gaining traction is the implementation of team topologies, which restructures t...]]></description><link>https://engineering.cloudflight.io/enhancing-test-automation-with-team-topologies-leveraging-enabling-teams</link><guid isPermaLink="true">https://engineering.cloudflight.io/enhancing-test-automation-with-team-topologies-leveraging-enabling-teams</guid><category><![CDATA[team topologies]]></category><category><![CDATA[test-automation]]></category><dc:creator><![CDATA[Marco Hampel]]></dc:creator><pubDate>Fri, 16 Feb 2024 07:30:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/7de474KZIbs/upload/52da9775c89f983a0bbd9bf113a94e91.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the fast-paced world of software development, teams are constantly seeking ways to optimize their processes and deliver high-quality products efficiently. One approach gaining traction is the implementation of <a target="_blank" href="https://teamtopologies.com/">team topologies</a>, which restructures teams to better align with organizational goals and improve collaboration. In this blog post, we'll explore how leveraging enabling teams can enhance test automation within the context of team topologies.</p>
<h3 id="heading-understanding-team-topologies"><strong>Understanding Team Topologies</strong></h3>
<p>Team topologies, introduced by Matthew Skelton and Manuel Pais, provide a framework for designing effective team structures within an organization. It emphasizes the need for teams to align with the organization's architecture and business objectives. The four fundamental team types are:</p>
<ul>
<li><p><strong>Stream-aligned Team</strong>: These teams focus on delivering value directly to the customer. They are cross-functional and have all the necessary skills to deliver end-to-end solutions. Stream-aligned teams own a specific part of the business or product, enabling faster decision-making and reducing dependencies on other teams.</p>
</li>
<li><p><strong>Enabling Team</strong>: Enabling teams provide support, tools, and platforms to streamline the work of stream-aligned teams and acting as a catalyst. They focus on creating self-service platforms, automation, and providing expertise to enable other teams to deliver efficiently.</p>
</li>
<li><p><strong>Complicated Subsystem Team</strong>: Some parts of the system are inherently complex and require specialized knowledge. Complicated subsystem teams are responsible for maintaining and evolving these parts of the system. They collaborate closely with stream-aligned teams, providing expertise and guidance when needed.</p>
</li>
<li><p><strong>Platform Team</strong>: Platform teams build and maintain shared platforms and services that streamline the work of other teams. They focus on creating reusable components, APIs, and tools that increase productivity and consistency across the organization. Platform teams abstract away common functionality, allowing stream-aligned teams to focus on delivering value.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707921022304/8c69c54b-263d-44ae-b697-b4547b0c5f11.png" alt class="image--center mx-auto" /></p>
<p>These team topologies are designed to foster effective communication (there will be a follow-up article, where the communication and interaction models will be discussed in more detail), collaboration, and autonomy within organizations. By aligning teams with the value streams of the business and providing the necessary support and expertise, organizations can improve their agility, speed, and ability to innovate.</p>
<h3 id="heading-enabling-teams-and-test-automation"><strong>Enabling Teams and Test Automation</strong></h3>
<p>Enabling teams play a crucial role in fostering collaboration and enabling other teams to succeed. When it comes to test automation, enabling teams can significantly impact the efficiency and effectiveness of testing efforts across the organization. Here's how:</p>
<ul>
<li><p><strong>Providing Test Automation Frameworks</strong>: Enabling teams can develop and maintain robust test automation frameworks tailored to the organization's needs.<br />  These frameworks can include libraries, tools, and best practices for writing and executing automated tests.</p>
</li>
<li><p><strong>Offering Training and Support</strong>: Enabling teams can offer training sessions and workshops to educate other teams on test automation best practices and tools. They can also provide ongoing support and guidance to help teams overcome any challenges they encounter during test automation implementation.</p>
</li>
<li><p><strong>Integrating Testing Tools</strong>: Enabling teams can integrate testing tools into the development workflow, making it seamless for teams to incorporate automated testing into their processes. This integration can include CI/CD pipelines, version control systems, and issue tracking tools.</p>
</li>
<li><p><strong>Promoting Collaboration</strong>: Enabling teams can facilitate collaboration between development and operations teams to ensure that test automation efforts are aligned with overall business goals. They can encourage cross-functional collaboration and knowledge sharing to improve the quality of automated tests.</p>
</li>
</ul>
<h3 id="heading-case-study"><strong>Case Study</strong></h3>
<p>Implementing Test Automation with an Enabling Team. Let's consider a hypothetical scenario where a software development company decides to implement test automation with the help of an enabling team.</p>
<p><strong>Challenges</strong></p>
<ul>
<li><p><strong>Manual Testing Overload:</strong> The team is overwhelmed with repetitive manual testing tasks and regression testing, leading to slower release cycles and increased risk of human errors.</p>
</li>
<li><p><strong>Inconsistent Testing Practices:</strong> There are inconsistencies in test execution, test coverage and reporting across different projects.</p>
</li>
<li><p><strong>Lack of Automation Expertise:</strong> The development teams lack the expertise and resources to implement and maintain test automation frameworks effectively.</p>
</li>
</ul>
<p><strong>A (possible) Solution</strong></p>
<p>To address these challenges, the software development company decides to form an enabling team dedicated to implementing test automation across the organization. The enabling team consists of experienced automation and quality engineers, and DevOps engineers who collaborate to build robust automation frameworks and provide support to development teams.</p>
<p><strong>Implementation Steps</strong></p>
<ul>
<li><p><strong>Assessment and Planning</strong></p>
<ul>
<li><p>The enabling team conducts a thorough assessment of existing testing processes, tools, and infrastructure.</p>
</li>
<li><p>They identify areas where automation can bring the most significant benefits and prioritize them based on impact and feasibility.</p>
</li>
</ul>
</li>
<li><p><strong>Framework Development</strong></p>
<ul>
<li><p>The enabling team designs and develops a flexible and scalable test automation framework tailored to the company's specific needs and technologies.</p>
</li>
<li><p>They choose appropriate testing tools and technologies based on the project requirements and industry best practices.</p>
</li>
</ul>
</li>
<li><p><strong>Training and Support</strong></p>
<ul>
<li><p>The enabling team conducts training sessions and workshops for development teams to educate them about test automation best practices, tools, and frameworks.</p>
</li>
<li><p>They provide ongoing support and guidance to help teams adopt automation seamlessly and address any challenges they encounter during the transition.</p>
</li>
</ul>
</li>
<li><p><strong>Integration with CI/CD Pipelines</strong></p>
<ul>
<li><p>The enabling team integrates test automation scripts into the company's continuous integration and continuous delivery (CI/CD) pipelines to automate the execution of tests as part of the build and deployment process.</p>
</li>
<li><p>They implement reporting and alerting mechanisms to provide real-time feedback on test results and identify issues early in the development lifecycle.</p>
</li>
</ul>
</li>
<li><p><strong>Monitoring and Maintenance</strong></p>
<ul>
<li><p>The enabling team establishes monitoring mechanisms to track the performance and effectiveness of automated tests.</p>
</li>
<li><p>They continuously monitor and maintain the automation framework, updating it as needed to accommodate changes in the software and technology landscape.</p>
</li>
</ul>
</li>
</ul>
<p><strong>Results</strong></p>
<ul>
<li><p><strong>Increased Test Coverage:</strong> Test automation significantly increases test coverage, allowing for more thorough testing of critical functionalities and edge cases.</p>
</li>
<li><p><strong>Faster Release Cycles:</strong> Automated tests reduce the time spent on manual testing, enabling faster and more frequent releases without compromising quality.</p>
</li>
<li><p><strong>Improved Quality:</strong> Automation leads to fewer defects in production, resulting in improved customer satisfaction and reduced support and maintenance overhead.</p>
</li>
<li><p><strong>Empowered Teams:</strong> Development teams feel empowered to focus on delivering value-added tasks, knowing that repetitive and time-consuming testing activities are automated.</p>
</li>
<li><p><strong>Continuous Improvement:</strong> The enabling team fosters a culture of continuous improvement, regularly evaluating and enhancing the test automation practices to keep pace with evolving technology and business needs.</p>
</li>
</ul>
<h3 id="heading-conclusion"><strong>Conclusion</strong></h3>
<p>In conclusion, leveraging enabling teams can greatly enhance test automation efforts within an organization. By providing frameworks, training, support, and promoting collaboration, enabling teams can empower other teams to implement and maintain effective automated testing practices. As organizations continue to prioritize quality and efficiency in software development, the role of enabling teams in driving successful test automation initiatives will become increasingly important.</p>
<blockquote>
<p>The series will continue with the next part focussing on communication and interaction patterns soon - stay tuned!</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Lakehouse: Securing data access]]></title><description><![CDATA[In this article, we'll delve into best practices for securing data access when using Microsoft Fabric Lakehouses. The goal is ensuring that your valuable data remains protected and enable people from within your organization to see subsets of the dat...]]></description><link>https://engineering.cloudflight.io/lakehouse-securing-data-access</link><guid isPermaLink="true">https://engineering.cloudflight.io/lakehouse-securing-data-access</guid><category><![CDATA[fabric]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[SQL]]></category><category><![CDATA[Power BI]]></category><category><![CDATA[Security]]></category><category><![CDATA[#reporting]]></category><category><![CDATA[column level security]]></category><category><![CDATA[row-level-security]]></category><category><![CDATA[lakehouse]]></category><category><![CDATA[data-warehousing]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[Stefan Starke]]></dc:creator><pubDate>Wed, 14 Feb 2024 17:00:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/zAjdgNXsMeg/upload/32320fa851265f378d27a89bf686d796.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, we'll delve into best practices for securing data access when using Microsoft Fabric Lakehouses. The goal is ensuring that your valuable data remains protected and enable people from within your organization to see subsets of the data that is relevant for their field of work. In the end we will have a look into sharing data with report creating users.</p>
<h3 id="heading-key-takeaways">Key Takeaways</h3>
<blockquote>
<p>Data access restriction is important.</p>
<p>Do not use data masking as a security measure.</p>
<p>Create reports based on shared semantic models.</p>
</blockquote>
<h3 id="heading-starting-point">Starting Point</h3>
<p>Let's assume we look at our Microsoft Fabric platform. We see fancy data pipelines that grep data from whatever sources you can think of, transform the hell out of the data leaving us with perfect, golden data inside a Lakehouse. Of course in form of nice delta tables.</p>
<p>Now how can we give data access to people within our organization and ensure that they</p>
<ul>
<li><p>Only see tables that are relevant for them? (Object Level Security)</p>
</li>
<li><p>Only see columns that are relevant for them? (Column Level Security)</p>
</li>
<li><p>Only see data (rows) that is relevant for them? (Row Level Security)</p>
</li>
</ul>
<p>Basically how can we enable them to query the data they are allowed to see, understand the data model and build reports on their own? Just in case you need it, the fancy buzzword is <em>Fostering self-service reporting while safeguarding data.</em></p>
<h3 id="heading-strategy">Strategy</h3>
<p>To realize this goal we adopted the following strategy:</p>
<ol>
<li><p><strong>Read-Only SQL Connection</strong>: Users are granted read-only access to the SQL endpoint of the lakehouse. By default, they are shielded from access to underlying tables, minimizing the risk of inadvertent alterations or unauthorized access.</p>
</li>
<li><p><strong>Row-Level Security (RLS)</strong>: RLS mechanisms are deployed to dynamically filter data at the row level based on user identities or membership in Microsoft 365 groups. This granular access control ensures that users only interact with data relevant to their designated roles or responsibilities.</p>
</li>
<li><p><strong>Column-Level Security (CLS)</strong>: Complementing RLS, CLS configurations restrict access to specific columns within tables, safeguarding sensitive information from unauthorized disclosure.</p>
</li>
<li><p><strong>Data Masking</strong>: As an additional layer of defense, data masking techniques are employed to obfuscate sensitive information displayed to users. Primarily cosmetic, this approach must not be compared to CLS as the risk of data leakage via brute-force queries is a given fact. See example lateron.</p>
</li>
<li><p><strong>Semantic Data Models for Self-Service Reporting</strong>: Empowering users with semantic data models serves as a structured abstraction layer, enabling them to navigate and interpret complex datasets intuitively. This approach fosters self-sufficiency in report creation while ensuring adherence to organizational data standards.</p>
</li>
</ol>
<h3 id="heading-share-sql-endpoint">Share SQL Endpoint</h3>
<p>Sharing the SQL endpoint of a lakehouse with recipients or groups is straight forward.</p>
<p>To give recipient a basic <em>Connect permission</em> like in an SQL server be sure to NOT select any of the available sharing options. By doing so per default no data is available to be read and GRANT permissions need to be explicity defined. Some kind of a <em>trust nobody</em> strategy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707482463673/7df53af0-6828-43db-980b-3f98375593ef.png" alt class="image--center mx-auto" /></p>
<p>For more details see <a target="_blank" href="https://blog.fabric.microsoft.com/en-us/blog/data-warehouse-sharing/">MS Data Warehouse Sharing</a>.</p>
<h3 id="heading-row-level-security">Row-Level Security</h3>
<p>Row-level security ensures that user can only access rows they are allowed to see.</p>
<p>These restrictions are applied directly inside the data tier (database) every time data is accessed. This makes it more reliable, robust and less error prone than restrictions that are applied inside an application tier.</p>
<p>To enable row-level security two things are needed</p>
<ul>
<li><p>a schema containing an inline table-valued function</p>
</li>
<li><p>a security policy using the created function as a predicate</p>
</li>
</ul>
<p><em>Let' have a look at a simple example.</em></p>
<p>First creating a new schema including a function that returns 1 when the value of the email is the same as the user executing the query. The value of the user is read via USER_NAME(). To make it more robust all values are converted to lowercase.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SCHEMA</span> sec;
GO

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">FUNCTION</span> sec.customers_validate_rlssecurity(@email <span class="hljs-keyword">as</span> <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">500</span>))
    <span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TABLE</span>
<span class="hljs-keyword">WITH</span> SCHEMABINDING
<span class="hljs-keyword">AS</span>
    <span class="hljs-keyword">RETURN</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-number">1</span> <span class="hljs-keyword">AS</span> validate_rlssecurity_result
    <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">LOWER</span>(@email) = <span class="hljs-keyword">LOWER</span>(USER_NAME())
<span class="hljs-keyword">GO</span>
</code></pre>
<p>Now all that is left to do is create a security policy, make the connection to the corresponding table (dbo.customers) and set its STATE to ON.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SECURITY</span> <span class="hljs-keyword">POLICY</span> CustomersFilter
<span class="hljs-keyword">ADD</span> FILTER PREDICATE sec.customers_validate_rlssecurity(Email)
<span class="hljs-keyword">ON</span> dbo.customers
<span class="hljs-keyword">WITH</span> (STATE=<span class="hljs-keyword">ON</span>);
GO
</code></pre>
<p>During development it can be useful to disable the policy temporarily. Just set the STATE to OFF.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">SECURITY</span> <span class="hljs-keyword">POLICY</span> CustomersFilter
<span class="hljs-keyword">WITH</span> (STATE = <span class="hljs-keyword">OFF</span>);
</code></pre>
<p>Looking into performance of RLS is most likely an additonal blog post on its own. Roughly the impact will be comparable to using a view. Two simple, basic recommendations to keep in mind</p>
<ul>
<li><p>Keep the predicate function simple. The more joins the worse the performance.</p>
</li>
<li><p>Ensure there are indices on referenced tables.</p>
</li>
</ul>
<h3 id="heading-column-level-security">Column-Level Security</h3>
<p>Nothing fancy here. Grant access to users, groups as needed.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">ON</span> customers(FirstName, LastName, Email) <span class="hljs-keyword">TO</span> [restricted.robert@cloudflight.dev];
</code></pre>
<p>It is important to know, that CLS in place will prevent restricted users from executing SELECT * statements like the one below as they will fail with an error message. Wanted columns need to be specified explicitly in the SELECT statement.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">from</span> customers;
</code></pre>
<pre><code class="lang-sql">Msg 230, Level 14, State 1, Line 12
The <span class="hljs-keyword">SELECT</span> permission was denied <span class="hljs-keyword">on</span> the <span class="hljs-keyword">column</span> <span class="hljs-string">'Age'</span> <span class="hljs-keyword">of</span> the <span class="hljs-keyword">object</span> <span class="hljs-string">'customers'</span>, <span class="hljs-keyword">database</span> <span class="hljs-string">'****'</span>, <span class="hljs-keyword">schema</span> <span class="hljs-string">'dbo'</span>.
</code></pre>
<p>In a way this may be seen as kind of leaking information as it tells the user that there are further columns and how they are named BUT at least they are not accessible.</p>
<p>It does however result in the direct SQL endpoint not being usable when creating a Power BI report. It will fail connecting to the endpoint with the same exceptions as above.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707483187573/90ef2c29-81a3-411d-8182-02a9ebad0a5c.png" alt class="image--center mx-auto" /></p>
<p>This issue can be solved by creating a semantic model of the lakehouse and use it as the base for creating a report. We will come to that later. Moreover it is the recommended approach when speaking about performance.</p>
<p>For more details see <a target="_blank" href="https://learn.microsoft.com/en-us/azure/synapse-analytics/sql-data-warehouse/column-level-security">MS Column-Level-Security</a>.</p>
<h3 id="heading-data-masking">Data Masking</h3>
<p>The main use case of data masking is to hide sensitive data in the result of a query.</p>
<p>It can be applied easily on columns by using a command like</p>
<pre><code class="lang-sql"><span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> dbo.customers;
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">COLUMN</span> City <span class="hljs-keyword">ADD</span> MASKED <span class="hljs-keyword">WITH</span> (<span class="hljs-keyword">FUNCTION</span> = <span class="hljs-string">'default()'</span>);
GO
</code></pre>
<p>Resulting, as expected, in queried data being masked.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707573295962/4bee50a2-a85a-4e37-94eb-d954c2eda97d.png" alt class="image--center mx-auto" /></p>
<p><strong>BUT</strong> there is a - from our point of view - rather alarming fact. One should never every use data masking as a replacement for column-level security. Data masking is applied once the data has been queried. This makes it possible to perform (automated) brute force queries for gathering insights into the masked data.</p>
<p>This is how - although data masking is in place - you can find out users from the marvelous, imaginary city <em>Burnsmouth</em>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707573607640/791dd7e3-eda9-45b5-904e-5f3de2a31195.png" alt class="image--center mx-auto" /></p>
<p>With having proper column-level security in place the user would not be able to get such insights as access to the city column would be denied completly.</p>
<p>Fore more details see <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-sql/database/dynamic-data-masking-overview?view=azuresql&amp;viewFallbackFrom=sql-server-ver16&amp;toc=%2Fazure%2Fsynapse-analytics%2Fsql-data-warehouse%2Ftoc.json">MS Dynamic Data Masking</a>.</p>
<h3 id="heading-building-reports-with-semantic-models">Building reports with semantic models</h3>
<p>Now that everything is secure, how can we best share data - including an easy to grasp data model - with someone eager to create reports?</p>
<p>The best way to achieve this is by sharing <strong>semantic models</strong>.</p>
<p><em>Let's look at some of the reasons why.</em></p>
<p>✔️ Firstly we have seen above, that with column-level security in place Power BI will fail accessing data from just an SQL endpoint as it is unable to query the structure of the tables. Most likely Power BI is doing a SELECT * FROM table to get all available columns which fails with proper column-level security in place.</p>
<p>✔️ Secondly a semantic model is a logical description of an extract of your lakehouse, most likely from a specific domain (e.g. Sales, HR). Most of the times it will be a star schema and can help report creating users easily understand the data model.</p>
<p>✔️ Thirdly the default connection type when using semantic models is the so called Direct Lake mode. It combines the existing connection types - Direct Query and Import - by combining their advantages. This means it is fast and there is no need for periodically updating your reports dataset to reflect the latest data changes. Going into detail would be a blog post on its own. For now you can find out more details <a target="_blank" href="https://learn.microsoft.com/en-us/power-bi/enterprise/directlake-overview">here</a>.</p>
<p>Every lakehouse creates a default semantic model but the recommendation is to create specific ones for individual use cases, narrowed down to the tables, relationships and columns the users need and are allowed to access (combined with column-level security).</p>
<p><em>Let's illustrate this process.</em></p>
<ul>
<li><p>Create a semantic model for a lakehouse</p>
</li>
<li><p>Include needed tables and model their relationships</p>
</li>
<li><p>Hide all columns that are not visible to the user because of column-level security (everything but the employee name from the dimension table)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707732764605/ad8a2244-dfe7-4b54-9ce1-5d817a6e1fc4.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Share the semantic model with a group of users</p>
</li>
<li><p>Let them import the semantic model in Power BI to build reports on top of it</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707732961901/16d383b5-e299-41e1-8b8c-706b19c3ae8e.png" alt class="image--center mx-auto" /></p>
<p>After connecting we see the expected result. The employee table only shows the employee column. Job done.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707733058892/6a783533-6f79-4ea9-8079-da418a15b1e3.png" alt class="image--center mx-auto" /></p>
<p>Remember, with column-level security in place connecting to the SQL endpoint would fail during import.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707734636880/4927126f-7d26-4caf-9e49-d90fbfd0102b.png" alt class="image--center mx-auto" /></p>
<p>That is it for now.</p>
<p>Stay tuned for more posts in the area of data engineering with Microsoft Fabric. Especially the important topic of how to automatically test granted permission and security in general might be an interesting next read.</p>
]]></content:encoded></item><item><title><![CDATA[Rapid Development with React-Admin and Fastify]]></title><description><![CDATA[(Written in collaboration with Mihai-Andrei Dancu)
Introduction
Software development is a time-intensive task and requires skilled software engineers to get the job done. Time and budget are directly proportional to one another and therefore as littl...]]></description><link>https://engineering.cloudflight.io/rapid-development-with-react-admin-and-fastify</link><guid isPermaLink="true">https://engineering.cloudflight.io/rapid-development-with-react-admin-and-fastify</guid><category><![CDATA[fastify]]></category><category><![CDATA[react-admin]]></category><category><![CDATA[mvp]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Peter Jedinger]]></dc:creator><pubDate>Fri, 13 Oct 2023 06:00:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/npxXWgQ33ZQ/upload/c479be11e889c4a8e035c0a32ca30ee3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>(Written in collaboration with <strong>Mihai-Andrei Dancu</strong>)</p>
<h1 id="heading-introduction">Introduction</h1>
<p>Software development is a time-intensive task and requires skilled software engineers to get the job done. Time and budget are directly proportional to one another and therefore as little development resources as possible should be wasted. Especially in small to mid-sized projects most development time should be spent on feature implementation and time spent on other tasks should be minimized.</p>
<p>As part of the <a target="_blank" href="https://cloudflight.hashnode.dev/navigating-efficient-web-application-development-cloudflights-architectural-insights">rapid development project</a> of the Cloudflight Technical Lab, we researched software solutions with a focus on quickly achieving results and developing a functional MVP in a very short time. Every team was tasked to develop a generic resource management system as described in the <a target="_blank" href="https://engineering.cloudflight.io/series/rapid-web">introduction</a> of this article series. Our team specifically built an interactive <a target="_blank" href="https://marmelab.com/react-admin/"><strong>React-Admin</strong></a> <strong>frontend</strong> and a <strong>NodeJS backend using</strong> <a target="_blank" href="https://fastify.dev/"><strong>Fastify</strong></a> with a PostgreSQL database. Our development insights are discussed in this article and the technologies are evaluated in terms of their suitability for rapid development.</p>
<h1 id="heading-tech-stack">Tech stack</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695737316901/0239fb81-3157-47be-a9d8-66a3e05b20f8.jpeg" alt class="image--center mx-auto" /></p>
<p>Besides the general resource management functionality, we implemented the sending of automated confirmation e-mails in the backend using Mailhog as a test mail server.</p>
<h1 id="heading-react-admin"><strong>React-Admin</strong></h1>
<p><a target="_blank" href="https://react.dev/">React</a> is a powerful web development framework with industry-wide adoption due to its clever component architecture and its capabilities to build interactive web pages. <a target="_blank" href="https://marmelab.com/react-admin/">React-Admin</a> builds on these attributes and adds useful extensions to simplify development and ensure maintainability. It is the promising bridge between custom high-code solutions and low/no-code solutions by reducing development time through abstracting existing React libraries.</p>
<p>The main benefit of React-Admin is its inclusion of numerous out-of-the-box Material UI components, which can be used to quickly build dashboard-styled web apps similar to YouTube Studio and Spotify. It also offers integrations to quickly implement authentication, permissions, internationalization and lots of other useful functionalities. React-Admin is best used for CRUD-based applications but can be limiting when developing complex web apps.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {Admin, Resource} <span class="hljs-keyword">from</span> <span class="hljs-string">"react-admin"</span>;
<span class="hljs-keyword">import</span> {SpotList} <span class="hljs-keyword">from</span> <span class="hljs-string">"../components/spots"</span>;
<span class="hljs-keyword">import</span> {dataProvider} <span class="hljs-keyword">from</span> <span class="hljs-string">"../providers/dataProvider"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> App = <span class="hljs-function">() =&gt;</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Admin</span> <span class="hljs-attr">dataProvider</span>=<span class="hljs-string">{dataProvider}</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Resource</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"spots"</span> <span class="hljs-attr">list</span>=<span class="hljs-string">{SpotList}/</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Admin</span>&gt;</span></span>
);
</code></pre>
<p><em>The core of React-Admin - the data provider specification, the declared resources (only one in this case) and the Admin component</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696344857739/8dc6836a-b16b-49a9-87e6-9b114167f8a8.png" alt class="image--center mx-auto" /></p>
<p><em>The SpotList is automatically fetched via DataProvider and all data is displayed in a table.</em></p>
<h2 id="heading-pros"><strong>Pros</strong></h2>
<ul>
<li><p>React-Admin components are a good base for dashboard-style apps</p>
</li>
<li><p>Components are styled and responsive by default</p>
</li>
<li><p>Routes for resource endpoints are configured automatically</p>
</li>
<li><p>Data provider abstraction allows for automatic API communication</p>
</li>
<li><p>Built-in role-based access control</p>
</li>
<li><p>Integrated internationalization (40+ locales supported)</p>
</li>
<li><p>Scalability assured by React's architecture</p>
</li>
</ul>
<h2 id="heading-cons"><strong>Cons</strong></h2>
<ul>
<li><p>Custom functionality (everything besides CRUD) can be difficult to implement</p>
</li>
<li><p>Backend API implementation has to follow data provider specifications</p>
</li>
<li><p>Some features and UI components are only available via an Enterprise Edition subscription (e.g. site-wide search, breadcrumb paths, AI autocomplete and more)</p>
</li>
<li><p>Little TypeScript documentation is available, mostly JavaScript</p>
</li>
</ul>
<h1 id="heading-fastify">Fastify</h1>
<p><a target="_blank" href="https://fastify.dev/">Fastify</a> is a modern NodeJS web framework that focuses on developer experience, low overhead and responsiveness by efficiently managing server resources. Its powerful plugin architecture allows developers to quickly implement features while requiring minimal configuration effort.</p>
<p>Plugins can be self-developed, but there are also core and community plugins, which can be added as dependencies. As an example, a “database connection plugin” can be registered in the application context and by using decorators it can then be accessed from anywhere in the application.</p>
<p>As a web framework, Fastify provides an easy solution to declare endpoints and routes for HTTP communication. Hooks can be used as event listeners to execute custom code, whenever a specific action is executed, e.g. a reply header is added before a request is returned.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fp = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fastify-plugin'</span>);

<span class="hljs-built_in">module</span>.exports = fp(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">fastify, opts, done</span>) </span>{
    fastify.addHook(<span class="hljs-string">"onRequest"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">request, reply</span>) </span>{
        reply.headers(
            {<span class="hljs-string">"Access-Control-Allow-Origin"</span>: <span class="hljs-string">"*"</span>,
             <span class="hljs-string">"Access-Control-Allow-Headers"</span>: <span class="hljs-string">"*"</span>,
             <span class="hljs-string">"Access-Control-Expose-Headers"</span>: <span class="hljs-string">"*"</span>}
        );
    });
    done()
})
</code></pre>
<p><em>A simple plugin that adds CORS headers to every reply</em></p>
<p>Moreover, Fastify requires very low overhead compared to similar frameworks. Only the core configuration file (“package.json”) for the Node.js eco-system is required and no additional boiler-plate code is needed. A "hello world" application can be written in ~10 lines of code.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fastify = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fastify'</span>)({ <span class="hljs-attr">logger</span>: <span class="hljs-literal">true</span> })
fastify.get(<span class="hljs-string">'/'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">request, reply</span>) </span>{
    reply.send({<span class="hljs-attr">hello</span>: <span class="hljs-string">'world'</span>})
})
fastify.listen({<span class="hljs-attr">port</span>: <span class="hljs-number">3000</span>}, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">err, address</span>) </span>{
    <span class="hljs-keyword">if</span> (err) {
        fastify.log.error(err)
        process.exit(<span class="hljs-number">1</span>)
    }
})
</code></pre>
<p><em>"Hello World" in Fastify</em></p>
<h2 id="heading-pros-1">Pros</h2>
<ul>
<li><p>Plugin architecture can accelerate development</p>
</li>
<li><p>Plugins can easily be reused in different projects</p>
</li>
<li><p>Fastify supports TypeScript (declaration file is maintained)</p>
</li>
<li><p>Fastify can serve up to 30k requests per second (claims by Fastify)</p>
</li>
<li><p>The plugin system allows an easy shift from monolithic applications to microservices (when the context is configured correctly)</p>
</li>
<li><p>Good official documentation resources</p>
</li>
</ul>
<h2 id="heading-cons-1">Cons</h2>
<ul>
<li><p>Understanding the application context and scope of the plugin registration can be complicated at first</p>
</li>
<li><p>Low overhead also means that all required functionality has to be self-implemented</p>
</li>
<li><p>Files can become very verbose (especially routes with parameter definitions)</p>
</li>
<li><p>Relatively small development community (few online discussions/threads)</p>
</li>
<li><p>No TypeScript documentation is available, only JavaScript</p>
</li>
</ul>
<h1 id="heading-lessons-learned">Lessons Learned</h1>
<p>Picking the right tech stack is one of the most important decisions when trying to accelerate the software development process. The best choice varies from project to project and it can be difficult to find the sweet spot between high and low/no-code solutions. High-code frameworks require lots of setup and configuration time but come with a lot of functionality out of the box (e.g. Spring Boot). Low-code solutions provide pre-built components that enable developers to bootstrap an application in minutes but might not be as flexible when custom functionality is required. Depending on the chosen technologies and frameworks a trade-off between flexibility and invested time always has to be made when considering development overhead.</p>
<p>The Fastify framework has very low overhead, but this also means that every required functionality needs to be self-implemented or at least self-configured if there is an existing plugin. The plugin architecture is a great system to accelerate development, especially if there is a baseline of existing plugins from previous projects. If a team is confident in working with it, Fastify can be a great framework choice.</p>
<p>React-Admin provides useful abstractions to quickly build simple CRUD-based dashboard-style apps in a few lines of code. If developers are working within the constraints of React-Admin, satisfactory results can be achieved rapidly, but implementing custom features can be very restrictive when working with the framework. Depending on the project scope, React-Admin can be a solid choice. It is best used for less complex projects with resource inspection and manipulation as the main focus.</p>
<p>Sadly there is no clear-cut solution to increase development speed by picking the right frameworks. All project requirements (and possible requirements in the future) need to be considered to make an educated decision. In the end, frameworks are just tools that should help developers fulfil the requirements, but suboptimal framework choices can negatively impact development time or even lead to project failure. The best framework choice is often the one teams are most experienced with because key concepts and limitations are known beforehand. When unfamiliar frameworks are proposed to be used, time should be spent on research and preparation to avoid problems in the future. If the correct framework is chosen for a specific use case, development speed can be increased significantly.</p>
<p>We hope that you learned something by reading this article and maybe gained a new perspective on framework choice. React-Admin and Fastify can be solid options and hopefully, you received some insight on whether they might be a good fit for one of your future projects. Keep on coding, cheers!</p>
]]></content:encoded></item><item><title><![CDATA[Microsoft Power BI]]></title><description><![CDATA[(Written in collaboration with Andreas Schweiger & Stefan Starke)
In today's data-driven world, organizations big and small rely on data to make informed decisions, gain insights, and drive business growth. However, raw data alone is seldom enough; i...]]></description><link>https://engineering.cloudflight.io/microsoft-power-bi</link><guid isPermaLink="true">https://engineering.cloudflight.io/microsoft-power-bi</guid><category><![CDATA[PowerBI]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[data analysis]]></category><category><![CDATA[visualization]]></category><category><![CDATA[data]]></category><dc:creator><![CDATA[Oguzhan Tuncer]]></dc:creator><pubDate>Fri, 06 Oct 2023 11:52:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/hGV2TfOh0ns/upload/d56682710d498708aea832eb562bbf60.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>(Written in collaboration with <strong>Andreas Schweiger</strong> &amp; <strong>Stefan Starke</strong>)</p>
<p>In today's data-driven world, organizations big and small rely on data to make informed decisions, gain insights, and drive business growth. However, raw data alone is seldom enough; it needs to be transformed into actionable information. This is where <a target="_blank" href="https://powerbi.microsoft.com/en-us/"><strong>Power BI</strong></a> steps in, offering a powerful suite of tools to analyze, visualize, and share data.</p>
<p>Before looking into the possibilities and techniques on how to embed Power BI reports - which is one of the most important questions in our custom software projects, let's have a brief look at the question:</p>
<p><em>What is Power BI and which key features does it offer?</em></p>
<p>In brief, <a target="_blank" href="https://powerbi.microsoft.com/en-us/">Power BI</a> is a business intelligence and data visualization tool that enables users to turn raw data into interactive visual reports and dashboards. It provides a unified platform for data exploration, data preparation, data modeling, and data sharing. With its intuitive interface and robust capabilities, Power BI has become the go-to choice for organizations seeking to extract meaningful insights from their data.</p>
<p>To give an idea, let's look at a sample report (which can be downloaded <a target="_blank" href="https://learn.microsoft.com/en-us/power-bi/create-reports/sample-customer-profitability">here</a>):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696331391134/506d91a6-93a0-4eb5-82d6-d6af22cd04ff.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-key-features">Key Features</h3>
<p><strong>1. Data Integration</strong></p>
<p>Power BI connects to a wide range of data sources, including databases, cloud services, and on-premises data. It can seamlessly integrate data from Excel spreadsheets, SQL databases, DataVerse, Azure, and many more, making it easy to consolidate and transform data from various sources into a single dataset.</p>
<p><strong>2. Data Transformation and Modeling</strong></p>
<p>Power BI offers a powerful data modeling engine that allows users to shape and transform data using the Power Query Editor. This tool is particularly useful for cleaning, filtering, and structuring data before analysis. Users can create relationships between tables, define calculated columns and measures, and apply advanced transformations.</p>
<p><strong>3.</strong> <strong>Interactive Visualization</strong></p>
<p>One of Power BI's standout features is its ability to create stunning visualizations. Users can choose from a wide range of charts, graphs, and maps to display data in a compelling and informative way. The drag-and-drop interface makes it easy to create interactive dashboards that update in real-time as data changes.</p>
<p><strong>4. Collaboration and Sharing</strong></p>
<p>Power BI enables collaboration by allowing users to share reports and dashboards with colleagues or external stakeholders. The ability to share reports and collaborate on data analysis fosters a culture of collaboration within organizations, promoting better knowledge sharing.</p>
<p><strong>5.</strong> <strong>AI and Machine Learning Integration</strong></p>
<p>Power BI integrates with Azure Machine Learning, allowing users to embed machine learning models into their reports and dashboards. This enables predictive analytics, anomaly detection, automated insights generation, and a Q&amp;A feature. It uses natural language processing to generate visualizations and answers based on the data available.</p>
<p><strong>6. Mobile Access</strong></p>
<p>With Power BI Mobile, users can access their reports and dashboards on smartphones and tablets. This ensures that decision-makers have access to critical data wherever they are.</p>
<h3 id="heading-licenses">Licenses</h3>
<p>Choosing the right Power BI license is crucial for maximizing the benefits of this powerful tool while staying within a defined budget.</p>
<p>In general, licensing can be split into two main categories, user-based (<strong>Free, Pro, Premium Per User</strong>) and capacity-based (<strong>Premium, Embedded</strong>) licensing.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>License Type</td><td>Target Users</td><td>Description</td><td>Cost</td></tr>
</thead>
<tbody>
<tr>
<td>Power BI Free</td><td>Anyone</td><td>It allows users to create reports and dashboards in Power BI Desktop, but it has limitations on sharing and collaboration. It's a good starting point for personal use or exploring Power BI's capabilities.</td><td>Free</td></tr>
<tr>
<td>Power BI Pro</td><td>Individual users who need to publish and share reports and dashboards within their organization</td><td>User-based license for report sharing and collaboration.</td><td>$9.99 per user per month</td></tr>
<tr>
<td>Power BI Premium</td><td>Organizations with larger user bases and more demanding requirements</td><td>It provides dedicated capacity for faster and more reliable performance. Pricing depends on the number of virtual cores and the amount of RAM allocated to the Premium capacity.</td><td>Variable, based on capacity and users.</td></tr>
<tr>
<td>Power BI Premium Per User (PPU)</td><td>Suitable for small to medium-sized businesses</td><td>User-based license that allows users to access premium features without the need for a full-scale Power BI Premium capacity.</td><td>$20 per user per month</td></tr>
<tr>
<td>Power BI Embedded</td><td>Developers and ISVs (Independent Software Vendors)</td><td>License for embedding Power BI reports and dashboards into custom applications. Pricing depends on the number of virtual cores and the amount of RAM allocated to the Premium capacity.</td><td>Variable, based on usage</td></tr>
</tbody>
</table>
</div><h2 id="heading-integrating-power-bi-reports">Integrating Power BI Reports</h2>
<p>If you are already developing applications or webpages based on the Power Platform ecosystem (<a target="_blank" href="https://engineering.cloudflight.io/microsoft-power-platform">CLF Engineering Blog</a>) then embedding a report into Power Apps is your way to go.</p>
<h3 id="heading-integrating-into-power-apps-or-power-pages">Integrating into Power Apps or Power Pages</h3>
<p>This integration allows you to embed Power BI content directly into web pages hosted on a Power Apps Portal, providing a seamless user experience for external customers, or users who may not have direct access to Power BI. Within your Power Apps Portal, you can embed Power BI reports or dashboards into web pages. This can be done using the "Power BI" component or HTML iframe.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696331717196/ac4bea9c-0329-4125-9124-d430b268a6de.png" alt class="image--center mx-auto" /></p>
<p>For more details, you can refer to the <a target="_blank" href="https://learn.microsoft.com/en-us/power-platform/">official documentation</a> or our <a target="_blank" href="https://engineering.cloudflight.io/microsoft-power-platform">Cloudflight Engineering Blog</a>.</p>
<h3 id="heading-integrating-into-a-custom-application">Integrating into a Custom Application</h3>
<p>For us - as a company developing custom software - the most interesting approach is to embed reports into other applications using Power BI Embedded.</p>
<p>To ensure that only authorized users can view embedded Power BI content, a robust authentication and authorization system is required and that is where Power BI Embedded Tokens come into play.</p>
<p>Power BI Embedded tokens are a type of security token that grants access to specific Power BI content. They are used to authenticate users and control their access to embedded reports and dashboards. Here's how they work:</p>
<ul>
<li><p><strong>Generate Token</strong>: When a user requests to view an embedded Power BI report or dashboard, the hosting application (our custom-developed applications) needs to authenticate the user with Power BI. It does this by requesting an embedded token.</p>
</li>
<li><p><strong>Token Parameters</strong>: The token request typically includes parameters such as the user's identity and roles and the specific report/dashboard to be accessed. These parameters determine what the user is allowed to see.</p>
</li>
<li><p><strong>Token Issuance</strong>: Power BI generates a token based on the provided parameters. This token is a temporary, time-limited access key.</p>
</li>
<li><p><strong>Access Control</strong>: The token contains information about the user's permissions, roles and the content they are allowed to access. When the user tries to access the embedded content, the token is validated to ensure the user has the necessary permissions.</p>
</li>
<li><p><strong>Expiration</strong>: Power BI Embedded tokens have a limited lifespan, which enhances security. Once the token expires, the backend must request a new one for continued access.</p>
</li>
</ul>
<p>Let's have a look at the steps we implemented to acquire embedded tokens:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696331916023/942c2270-8c80-4699-97a8-5f4695b00b22.jpeg" alt class="image--center mx-auto" /></p>
<ol>
<li><p><strong>User Authentication</strong>: Firstly, the user of your web application goes through an authentication process within your web app using your chosen authentication method. This step verifies the user's identity.</p>
</li>
<li><p><strong>Web App Authorization</strong>: Our web application, having successfully authenticated the user, utilizes a service principal to establish authentication with Azure Active Directory (Azure AD). This step grants our web app the necessary permissions for interaction with Power BI REST APIs by requesting an Azure AD token.</p>
</li>
<li><p><strong>Embed Token Request</strong>: Our web application communicates with the Power BI Embed Token REST API operation, initiating a request for an embed token. This specific token defines precisely which Power BI content can be embedded within our application as explained above. In response to our request, the REST API provides our web application with the embed token, which is specific to the requested Power BI content.</p>
</li>
<li><p><strong>Passing the Embed Token</strong>: Our web application then securely passes this embed token to the user's web browser, allowing the user's browser to facilitate the interaction with Power BI.</p>
</li>
<li><p><strong>User Access</strong>: Finally, the web app user employs the embed token within their browser to access and interact with Power BI content, as authorized by the token's permissions.</p>
</li>
</ol>
<p>Feel free to have a look at the <a target="_blank" href="https://github.com/microsoft/PowerBI-Developer-Samples">source code</a> provided by Microsoft to see how you can acquire an Azure AD token using a service principal and how to generate embed tokens.</p>
<p>Regarding the frontend integration, Power BI supports all major UI frameworks - Angular, <a target="_blank" href="https://learn.microsoft.com/en-us/javascript/api/overview/powerbi/powerbi-client-vue">VueJS</a> and <a target="_blank" href="https://learn.microsoft.com/en-us/javascript/api/overview/powerbi/powerbi-client-react">React</a>.</p>
<pre><code class="lang-javascript">&lt;powerbi-report
    [embedConfig] = {{
        <span class="hljs-attr">type</span>: <span class="hljs-string">"report"</span>,
        <span class="hljs-attr">id</span>: <span class="hljs-string">"&lt;Report Id&gt;"</span>,
        <span class="hljs-attr">embedUrl</span>: <span class="hljs-string">"&lt;Embed Url&gt;"</span>,
        <span class="hljs-attr">accessToken</span>: <span class="hljs-string">"&lt;Access Token&gt;"</span>,
        <span class="hljs-attr">tokenType</span>: models.TokenType.Embed,
        <span class="hljs-attr">settings</span>: {
            <span class="hljs-attr">panes</span>: {
                <span class="hljs-attr">filters</span>: {
                    <span class="hljs-attr">expanded</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">visible</span>: <span class="hljs-literal">false</span>
                }
            },
            <span class="hljs-attr">background</span>: models.BackgroundType.Transparent,
        }
    }}

    [cssClassName] = { <span class="hljs-string">"reportClass"</span> }

    [phasedEmbedding] = { <span class="hljs-literal">false</span> }

    [eventHandlers] = {
        <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>([
            [<span class="hljs-string">'loaded'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Report loaded'</span>);],
            [<span class="hljs-string">'rendered'</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Report rendered'</span>);],
            [<span class="hljs-string">'error'</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(event.detail);]
        ])
    }
&gt;
&lt;/powerbi-report&gt;
</code></pre>
<h2 id="heading-row-level-security">Row Level Security</h2>
<p><strong>Row Level Security (RLS)</strong> in Power BI is a security feature that allows you to control access to data at the row level based on user roles and filters. This means you can restrict what data individual users or groups of users can see within a Power BI report or dataset.</p>
<ul>
<li><p>RLS is typically implemented by creating user roles within your Power BI model. Each user role can have specific data access rules associated with it. User roles can be defined and managed in Power BI Desktop or through Power BI service.</p>
</li>
<li><p>To enforce RLS, you use filter expressions within user roles. These filter expressions are written in a DAX (Data Analysis Expressions) language and define which rows of data are visible to users in that role. Filter expressions can be as simple or complex as needed, allowing you to create dynamic filters based on user attributes, such as username or department. You can find an example usage of DAX expressions where the users will see their data rows ONLY if the underlying data set has a record under their username.</p>
</li>
<li><p>RLS provides dynamic security, meaning that the data is filtered in real time as users interact with the report or dataset. Users will only see the data that aligns with their role and the applied filters. The filters can be applied to specific tables and roles as follows:</p>
</li>
<li><p>Once RLS rules are defined and tested, you can publish your Power BI report or dataset to the Power BI service. RLS rules are enforced in the service as well, ensuring consistent security across different platforms and devices.</p>
</li>
</ul>
<p><strong>Example of using RLS for filtering per person:</strong></p>
<p>We want to filter everything in the <a target="_blank" href="https://learn.microsoft.com/en-us/power-bi/create-reports/sample-customer-profitability">sample report</a> such that the logged-in executive can only see their data. For that, we will have to use <strong>USERPRINCIPALNAME()</strong> in DAX which will return the logged-in executive. Create a new measure on the table that has the executive names and name it "User" with the value <strong>USERPRINCIPALNAME()</strong>. Then, create the security role by clicking on the Modeling tab -&gt; Manage Roles. Create a role and then define the filter. This filter simply means that the logged-in user will only see his/her records in the whole data set:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696332296970/133f6828-c1c4-41aa-92e5-38110e39debd.png" alt class="image--center mx-auto" /></p>
<p>This is how the sample report looks when we view it as the executive "Andrew Ma" after RLS:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696332315533/364b94b8-db3d-4d57-b3cc-713137d78bb1.png" alt class="image--center mx-auto" /></p>
<p><strong>Example of using RLS to hide/show items depending on the security role:</strong></p>
<p>A function to hide an entire page or specific elements in a report for certain roles is not available but there is a workaround on PowerBI Desktop. You can build your report as normal, then add a card and make it big, so that it overlays all the things you want to hide.</p>
<p><strong>Warning:</strong> Note that this workaround would only visually hide the report from the UI and a malicious user would still have access to the data in the underlying dataset. Therefore, it should not solely be employed in combination with RLS when working with real data.</p>
<p>To do that, enter new data and create the following table:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696332358852/ac24da95-47a6-4f94-bea5-9692a6eb8df0.png" alt class="image--center mx-auto" /></p>
<p>Create a new measure and write the following DAX which creates the message you want to display:</p>
<ul>
<li>Message = IF(HASONEFILTER('RLS Table'[RLS]), "You are not authorized!", "")</li>
</ul>
<p>Create another measure and write the following DAX which controls the background of the overlay card we will create:</p>
<ul>
<li>Make Transparent = IF(HASONEFILTER('RLS Table'[RLS]), "#White", "#FFFFFF00")</li>
</ul>
<p>Create a card and make it as big as the report page. Choose the "Message" measure to be displayed on it.</p>
<p>Format the RLS Table's visual in the general tab and find the background properties. Set the first dropdown to "Field value" and the second to "Make Transparent".</p>
<p>Create a new role that will not be authorized to see the report:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696332410323/04f02a69-d90b-4e15-a889-6acb632e2621.png" alt class="image--center mx-auto" /></p>
<p>This is how the report should look like when you make the overlay card as big as the report:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696332424537/5ef5fd66-ea42-4e52-af62-43fe81d3c8b4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Power BI is a powerful tool that empowers organizations to harness the full potential of their data. With the possibilities for embedding reports into applications, it is a valuable puzzle piece in our toolkit when it comes to efficiently implementing custom software solutions.</p>
]]></content:encoded></item><item><title><![CDATA[Reliable communication using the Transactional Outbox Pattern]]></title><description><![CDATA[In today's digital age, email communication remains an indispensable tool for businesses and individuals alike. Whether it's sending important notifications, marketing campaigns, or transactional updates, emails play a pivotal role in ensuring effect...]]></description><link>https://engineering.cloudflight.io/reliable-communication-using-the-transactional-outbox-pattern</link><guid isPermaLink="true">https://engineering.cloudflight.io/reliable-communication-using-the-transactional-outbox-pattern</guid><category><![CDATA[Reliability]]></category><category><![CDATA[SES]]></category><category><![CDATA[smtp]]></category><category><![CDATA[design patterns]]></category><category><![CDATA[Transactional outbox pattern]]></category><dc:creator><![CDATA[Andrei Cotor]]></dc:creator><pubDate>Fri, 29 Sep 2023 12:13:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/LPZy4da9aRo/upload/5e50aca8ff3428a435b8bdb65dca9eeb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In today's digital age, email communication remains an indispensable tool for businesses and individuals alike. Whether it's sending important notifications, marketing campaigns, or transactional updates, emails play a pivotal role in ensuring effective communication.</p>
<p>At first glance sending an email is just a line of code, right? Well, integrating any asynchronous messaging functionality (e.g. sending emails, sending data to 3rd party services, billing systems etc.) into software applications can be a daunting task, especially when it comes to ensuring the reliability and consistency of message delivery. Most of the time applications need a way to send one message if and only if the database gets updated with an entry. A simple example of when this behavior is necessary would be user registration.  When a new user registers, the application has to send a confirmation email to their address. Seems easy enough, so what could go wrong?</p>
<p>Let us try writing some Spring Boot code with Kotlin to illustrate the problem:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Transactional</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">registerUser</span><span class="hljs-params">(user: <span class="hljs-type">User</span>)</span></span>{
    userRepo.save(user)
    emailService.send(ConfirmationEmail())
}
</code></pre>
<p>At first sight, this code may seem right. But what happens if the server encounters an error after saving the new user? The email will get sent but the user will not exist in the database. Now, you might think of wrapping this code in a try-catch block such that we don't execute the send function if the save operation fails. This would look something like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Transactional</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">registerUser</span><span class="hljs-params">(user: <span class="hljs-type">User</span>)</span></span>{
    <span class="hljs-keyword">try</span> {
        userRepo.save(user)
        emailService.send(ConfirmationEmail())
    }
    <span class="hljs-keyword">catch</span>(err: UserRepoException) {
        <span class="hljs-comment">// ...    </span>
    }
}
</code></pre>
<p>Unfortunately, this isn't a good approach either. What if the email provider is not available at the moment? The user will be persisted in the database, but the confirmation email will never arrive to them. It will not even be sent to begin with. This seems unacceptable in a professional, modern web application. We would like to somehow roll back the saving of the user if sending of the email fails. This might remind you of the lesson about transactions from the databases course.</p>
<h2 id="heading-transactional-outbox-pattern-to-the-rescue">Transactional Outbox Pattern to the Rescue</h2>
<p>The <a target="_blank" href="https://microservices.io/patterns/data/transactional-outbox.html">Transactional Outbox Pattern</a>, a proven architectural design, provides a robust and reliable solution for managing email sending within applications. It addresses various use cases where message delivery is crucial and helps mitigate potential failures that can occur when this pattern is not implemented.</p>
<p>The basic idea is to have an Outbox table that contains the emails our application has to send out. We can now insert, <strong>in the same transaction</strong>, into this new table and the User table when a new user is created.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695804617689/0380344a-cf23-4f22-be5d-4e5e3aa76e6f.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-key-components-of-the-transactional-outbox-pattern">Key Components of the Transactional Outbox Pattern:</h3>
<ol>
<li><p><strong>Outbox:</strong> The central component of this pattern is the "outbox," which acts as a temporary storage for messages that need to be sent. When a message needs to be sent (e.g. an email), instead of sending it directly, it is first placed in the outbox, <strong>in the same transaction as the update on the other table</strong> (e.g. Insert in the User table). This outbox can be implemented as a database table, a message queue, or any other persistent storage. In our case, it will be a database table.</p>
</li>
<li><p><strong>Message Queue or Scheduler:</strong> An essential part of the pattern is a mechanism that monitors the outbox for messages and sends them at an appropriate time. This can be done using a message queue (e.g. RabbitMQ, Kafka) or a scheduler that periodically checks the outbox for pending messages. When a message is sent successfully, it is marked as "sent" in the outbox.</p>
</li>
<li><p><strong>Transactional Behavior:</strong> The Transactional Outbox Pattern ensures that message sending is part of a larger transaction as an atomic database operation. If the transaction fails (e.g., due to an error or an exception), the messages are not inserted into the outbox and the data is not updated in the other table (UsersTable in our example). This guarantees that messages are only sent when the whole transaction is successful, maintaining data consistency. This behavior can be easily achieved in Spring JPA using the <code>@Transactional</code> annotation since it satisfies the "<a target="_blank" href="https://mariadb.com/resources/blog/acid-compliance-what-it-means-and-why-you-should-care/">ACID</a>" requirements: it is Atomic, Consistent, Isolated and Durable.</p>
</li>
</ol>
<h3 id="heading-sequence-diagram">Sequence Diagram</h3>
<p>We can illustrate the flow of registering a new user in an application that implements the Transactional Outbox pattern. It would look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695804776001/b1a2ffd1-e7cd-4319-811d-55b3211707f5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-implementation">Implementation</h2>
<p>Now that we understand the problem and have a solution, we can write an EmailService class to achieve reliable email sending:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TransactionalEmailService</span></span>(
    <span class="hljs-keyword">val</span> outboxRepository: OutboxRepository,
    <span class="hljs-keyword">val</span> emailOutboxMapper: EmailOutboxMapper
) {
    <span class="hljs-meta">@Transactional</span>
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">sendEmail</span><span class="hljs-params">(email: <span class="hljs-type">Email</span>)</span></span> {
        <span class="hljs-keyword">this</span>.outboxRepository.save(<span class="hljs-keyword">this</span>.emailOutboxMapper.emailToOutbox(email))
    }
}
</code></pre>
<p>The service has only one function: <code>sendEmail()</code>. The function saves the Email object into the Outbox table as a database entry. It is annotated with <code>@Transactional</code>, allowing Spring to handle the database save operation as part of a transaction. Please note that the default propagation for <code>@Transactional</code> is required since we want the code in the <code>sendEmail()</code> function to run in the same transaction as the code in the function calling it (so both the original database operation and the save on the outbox repository happen in the same transaction). Using another propagation might result in creating a second transaction for <code>sendEmail()</code>, which would defeat the purpose. For more information please check the <a target="_blank" href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html">official documentation</a>.</p>
<h3 id="heading-scheduled-task-email-relay">Scheduled task (Email Relay)</h3>
<p>To fetch the Outbox table for new entries we decided to use a scheduled task. This task fetches batches (pages) of entries from the Outbox until there aren't any un-fetched entries. It is important to implement this way and to not only fetch one batch per execution since, if at any point a lot of messages enter the Outbox (if a newsletter has to be sent, for example), the scheduled task will not stop until it has tried to send every message, thus saving the time it would have had to wait between the scheduled runs.</p>
<p>We should also mention that in the Outbox table, besides the usual email fields, we also store:</p>
<ul>
<li><p>the scheduled date to send the email, which serves two purposes: to send emails at a certain point in the future; and if sending fails, mark at which point a resend should be retried</p>
</li>
<li><p>number of tries: if an email fails too many times the service will stop trying to send it</p>
</li>
</ul>
<p>To optimize our selects, we created a compound index on the Outbox table on the columns ID, Number of tries and Scheduled date.</p>
<p>A pitfall we have to watch out for is that we may have multiple instances of the scheduled task running at once. To make sure that the two tasks don't select the same batch of emails (which would result in sending the same emails multiple times) we can use a distributed lock, like <a target="_blank" href="https://github.com/lukas-krecan/ShedLock">ShedLock</a>. Its purpose is to make sure only one instance of the scheduled task is running at a certain point in time. Another way, which is a bit more database management system specific is using the row locks FOR UPDATE SKIP LOCKED. This command is available for <a target="_blank" href="https://www.postgresql.org/docs/current/sql-select.html#:~:text=such%20a%20case.-,The,-Locking%20Clause">PostgreSQL</a>, <a target="_blank" href="https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/innodb-locking-reads.html">Oracle</a> and others but not present for example in SQL Server and SQLite.</p>
<p>A pseudocode of our implementation would look something like this:</p>
<pre><code class="lang-kotlin">acquire distributed lock
repeat until no batches left {
    batch = select relevant entries from Outbox table
    <span class="hljs-keyword">for</span> each email <span class="hljs-keyword">in</span> batch {
        <span class="hljs-keyword">try</span> {
            send(email)
            <span class="hljs-comment">// send - successful</span>
            delete email from Outbox table
        }
        <span class="hljs-keyword">catch</span> {
            <span class="hljs-comment">// send - failed</span>
            email -&gt; increase number of tries
            email -&gt; <span class="hljs-keyword">set</span> scheduled send date sometime <span class="hljs-keyword">in</span> future   <span class="hljs-comment">// (time of retry)</span>
            update email <span class="hljs-keyword">in</span> Outbox table       
        }
    }
}
release distributed lock
</code></pre>
<h3 id="heading-email-sender">Email Sender</h3>
<p>There are multiple approaches to handling this step, and it is very project-specific. You might use an SMTP server or something like AWS SES. At this point, there isn't anything that can go wrong as long as you are careful to catch all exceptions in the Scheduled Task so that they are handled properly.</p>
<p>We created a custom AWS SES Sender class that implements JavaMailSender. This way we can easily switch between the default JavaMailSenderImpl SMTP implementation and AWS SES implementation just by changing a configuration. This is Spring Boot specific, but it should be pretty similar in other languages and frameworks. Our implementation looks something like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@ConditionalOnProperty(
    value = [<span class="hljs-meta-string">"email.sender"</span>],
    havingValue = <span class="hljs-meta-string">"AWS"</span>,
    matchIfMissing = false
)</span>
<span class="hljs-meta">@Component</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AWSSESJavaMailSender</span></span>(
    <span class="hljs-meta">@Autowired</span> <span class="hljs-keyword">val</span> sesClient: AmazonSimpleEmailService
): JavaMailSender {

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">send</span><span class="hljs-params">(mimeMessage: <span class="hljs-type">MimeMessage</span>)</span></span> {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">val</span> outputStream = ByteArrayOutputStream()
            mimeMessage.writeTo(outputStream)

            <span class="hljs-keyword">val</span> buf = ByteBuffer.wrap(outputStream.toByteArray())

            <span class="hljs-keyword">val</span> rawMessage = RawMessage(buf)

            <span class="hljs-keyword">val</span> rawEmailRequest = SendRawEmailRequest(rawMessage)

            sesClient.sendRawEmail(rawEmailRequest)
        }
        <span class="hljs-keyword">catch</span> (ex: Exception) {
            <span class="hljs-keyword">throw</span> MailSendException(<span class="hljs-string">"Could not send email through AWS SES"</span>, ex)
        }
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createMimeMessage</span><span class="hljs-params">()</span></span>: MimeMessage {
        <span class="hljs-keyword">return</span> MimeMessage(Session.getDefaultInstance(Properties()))
    }

    <span class="hljs-comment">// other functions' from  JavaMailSender implementation...</span>
}
</code></pre>
<h3 id="heading-usage">Usage</h3>
<p>The purpose of this project was to implement the pattern in an easily reusable way for our Spring Boot projects. We provided a way to make the pattern easy to use while keeping the code clean.</p>
<p>The <code>sendEmail()</code> function of the TransactionalEmailService can be called in any other transactional function:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExampleEmailServiceImpl</span></span>(
    <span class="hljs-keyword">val</span> userRepo: UserRepo,
    <span class="hljs-keyword">val</span> emailService: TransactionalEmailService
){
    <span class="hljs-meta">@Transactional</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">exampleUsage</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">this</span>.userRepo.save(User(...))
        <span class="hljs-keyword">val</span> email = Email(...)
        <span class="hljs-keyword">this</span>.emailService.sendEmail(email)
  }
}
</code></pre>
<p>Note that both the <code>exampleUsage()</code> function and our <code>sendEmail()</code> function are annotated with <code>@Transactional</code>. Spring is smart enough to handle both database changes in a single transaction, fulfilling the ACID requirements.</p>
<p>For a more decoupled approach we can make use of event listeners:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExampleUserService</span></span>(
    <span class="hljs-keyword">val</span> userRepo: UserRepo,
    <span class="hljs-keyword">val</span> eventPublisher: ApplicationEventPublisher
){
    <span class="hljs-meta">@Transactional</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">exampleUsage</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">val</span> user = userRepo.save(User(...))
        eventPublisher.publish(UserCreatedEvent(user))
    }
}

<span class="hljs-meta">@Service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserEventListener</span></span>(
    <span class="hljs-keyword">val</span> emailService: TransactionalEmailService
) {
    <span class="hljs-meta">@Transactional</span>
    <span class="hljs-meta">@EventListener</span>
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">handleUserCreatedEvent</span><span class="hljs-params">(event: <span class="hljs-type">UserCreatedEvent</span>)</span></span> {
        <span class="hljs-keyword">val</span> email = Email(...)
        emailService.sendEmail(email);
    }
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If right now you're thinking about that project that you worked on in the past and you didn't implement information sending in a truly reliable way, you're not alone. This is a very common mistake unfortunately, but we hope that through this article we were able to paint a clearer picture about this design pattern, why it is useful and how to implement it.</p>
]]></content:encoded></item><item><title><![CDATA[Microsoft Power Platform]]></title><description><![CDATA[(Written in collaboration with Gerasimos Fousekis & Stefan Starke)
While our roots are firmly planted in traditional software development methodologies, our commitment to innovation and client satisfaction drives us to continuously explore new horizo...]]></description><link>https://engineering.cloudflight.io/microsoft-power-platform</link><guid isPermaLink="true">https://engineering.cloudflight.io/microsoft-power-platform</guid><category><![CDATA[Strapi]]></category><category><![CDATA[microsoft power platform]]></category><category><![CDATA[PowerPages]]></category><category><![CDATA[Vue.js]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Alex Ciosa]]></dc:creator><pubDate>Mon, 25 Sep 2023 13:54:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/hpjSkU2UYSU/upload/ef7096bc4a3c59e11705d40e68a3e842.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>(Written in collaboration with <strong>Gerasimos Fousekis</strong> &amp; <strong>Stefan Starke</strong>)</p>
<p>While our roots are firmly planted in traditional software development methodologies, our commitment to innovation and client satisfaction drives us to continuously explore new horizons. This leads us to consider various emerging platforms, like <strong>Microsoft Power Platform,</strong> as potential additions to our already extensive toolkit.<br />While this might seem a bit unconventional for a software development company, our motivation is rooted in a deeper understanding of the ever-evolving business landscape and the diverse needs of our clients.</p>
<p><em>"What was your goal?",</em> you ask.</p>
<p>In brief:</p>
<ul>
<li><p><strong>Fast results</strong></p>
<p>  How much faster can we implement solutions when working in an environment with topics such as authentication, infrastructure and deployment, more or less out of the box?</p>
</li>
<li><p><strong>Customer empowerment</strong></p>
<p>  Can the use of something like <strong>Power Platform</strong> help clients maintain their applications, run updates, and introduce new features with little to no prior coding knowledge?</p>
</li>
<li><p><strong>Project diversity</strong></p>
<p>  Which projects do we see this approach as being beneficial, optimal even?<br />  Are there any blockers that could prevent us from following this approach?</p>
</li>
</ul>
<h1 id="heading-overview"><strong>Overview</strong></h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693492084740/479a9ba7-3502-4a56-a9c5-ed377c3cb8a3.png" alt="Power Platform Architectural Overview" class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://learn.microsoft.com/en-us/power-pages/admin/architecture">Power Platform Architecture</a></p>
<p><a target="_blank" href="https://powerplatform.microsoft.com/en-us/"><strong>Microsoft Power Platform</strong></a> is a suite of integrated tools and services designed to assist individuals and organizations in creating custom business applications, automating workflows, analyzing data and generating reports.<br />It's comprised of four core components:</p>
<ol>
<li><p><strong>Power Apps &amp; Power Pages</strong><br /> They allow users to create custom applications without extensive coding knowledge, to begin with. They offer canvas apps and model-driven apps: canvas apps enable building applications with a visual interface, while model-driven apps are more data-centric and built on the Common Data Service. Both interact with (business) data stored in the Dataverse with the main difference being that Power Pages is targeted at external users backed by the many authentication options (e.g. <em>Azure AD, LinkedIn, Facebook</em>) that are available out of the box.</p>
</li>
<li><p><strong>Power Automate</strong><br /> Formerly known as <strong>Microsoft Flow</strong>, enables the automation of repetitive tasks and workflows across various applications and services. It connects to hundreds of apps and services, allowing users to create automated processes without much complexity (e.g. automated email sending).</p>
</li>
<li><p><strong>Power BI</strong><br /> A powerful business analytics tool that allows users to visualize data, generate reports and build dynamic dashboards. It's particularly useful for data analysis and decision-making by turning raw data into actionable insights.</p>
</li>
<li><p><strong>Power Virtual Agents</strong><br /> Allows users to create chatbots that can be used to engage with customers, provide support, answer queries and automate various interactions.</p>
</li>
</ol>
<p><a target="_blank" href="https://engineering.cloudflight.io/navigating-efficient-web-application-development-cloudflights-architectural-insights">Aiming to fulfill the requirements mentioned previously</a>, we ultimately focused on the use of <strong>Power Pages</strong> and <strong>Power Automate</strong>.</p>
<h1 id="heading-pros-andamp-cons"><strong>Pros &amp; Cons</strong></h1>
<table><tbody><tr><td><p>PROS</p></td><td><p>CONS</p></td></tr><tr><td><p>Smaller, data-driven applications can be <em>clicked together</em> really quickly</p></td><td><p>Generally useful features (i.e. manual code changes, template presets) are better avoided altogether due to limited usability</p></td></tr><tr><td><p>Out-of-the-box ecosystem (e.g. infrastructure, scaling, IP whitelisting etc.)</p></td><td><p>Frequently unreliable and lengthy loading times during development, especially when reloading the Designer</p></td></tr><tr><td><p>Built-in application lifecycle management (e.g. environments with pipelines)</p></td><td><p>Cost can become very high for projects with a larger audience</p></td></tr><tr><td><p>Built-in i18n capabilities</p></td><td><p>Changes performed directly on the tables inside the Dataverse (e.g. scheduled job manipulating some rows) can take up to 15 minutes to apply</p></td></tr><tr><td><p></p></td><td><p>Limited monitoring capabilities and having no way to see where anonymous users come from (i.e. IP addresses)</p></td></tr></tbody></table>

<p>Next, we'll share some useful insights gained along the way.</p>
<h2 id="heading-pricing-andamp-costs"><strong>Pricing &amp; Costs</strong></h2>
<p>First, a word of advice - make sure to understand the licensing and billing of the entire <strong>Power Platform</strong> ecosystem to avoid <em>unpleasant and costly surprises</em>. In our setup, the main cost driver (besides Dataverse storage) was the amount of monthly active users, both anonymous and authenticated. Prices vary depending on the chosen subscription plan, meaning either a package of a set number of users or the option of "<em>pay as you go</em>" (PAYG). Technically, we used a billing policy linked directly to an active Azure subscription.<br />That being said: <strong>be aware that current costs are not immediately visible and it can take up to 24 hours until they become available within Azure cost management.</strong></p>
<blockquote>
<p>"Why is there so much emphasis on pricing?", you might be asking.</p>
<p>Imagine a scenario where someone sets up a basic cron job that operates on the Dataverse every minute or so. A premium flow that can either run in the cloud or attended costs $0.60 per run, mind you.</p>
<p>We'll let you do the math and figure out how high the costs get when the cost management alerts start triggering, which - as explained above - can take up to 24 hours to be updated.</p>
<p>Spoiler alert: It's $864.</p>
<p>Use a Power Automate license in similar scenarios.</p>
</blockquote>
<p>Speaking of <em>monthly active users</em>...</p>
<p>They are reported in a daily summary, downloadable within the <strong>Power Admin Portal</strong>.<br />Considering the uniqueness of an anonymous user being tracked using a browser cookie, we see a potential risk of unpredictable costs as a consequence. The claim is that malicious attacks, bots and crawlers are excluded from being counted, but we could not 100% confirm this statement.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693494050500/bbcccad0-1ca6-4118-9202-55f10ff25509.png" alt="User Access Report" /></p>
<p><strong><em>TL;DR</em></strong></p>
<ul>
<li><p>Understanding the main use case for the application, with regards to target users, is highly advised to avoid <strong>precarious financial situations</strong></p>
</li>
<li><p>Make sure to choose the optimal licensing model from the very beginning</p>
</li>
<li><p>As in all cloud projects: <em>enable cost alerts from Day One!</em></p>
</li>
</ul>
<h2 id="heading-development"><strong>Development</strong></h2>
<p>As with every new technology, there will be a learning curve. It's not particularly steep when it comes to the <strong>Power Platform</strong> itself, but one has to be wary that the workflow is not comparable to a 'traditional' software development process. Coupled with the documentation provided by Microsoft, which sadly tends to be <em>a bit outdated</em> at times, this can be a cause for sudden spikes in the overall learning curve.</p>
<p>Coming into this methodology, it took us some time to get used to the way collaborative work can be set up on a shared Power Pages environment. Because there's no implementation of version control, merge requests or individual commits, it took further coordination to avoid interfering with one another. This is easily manageable for smaller-scale projects, though not recommended for larger projects. In turn, we recommend a team size of no more than 3 people for optimal workflow.</p>
<p>One undisputable upside is that coming from a background of data-driven applications with CRUDL (Create-Read-Update-Delete-List), overall development time is <em>crazy fast</em>. It essentially boils down to "<em>creating tables, views and forms (rinse and repeat)</em>". Development is further sped up by the use of provided components for features, such as:</p>
<ul>
<li><p>Login</p>
</li>
<li><p>User Registration</p>
</li>
<li><p>Authentication Providers</p>
</li>
</ul>
<p><strong><em>TL;DR</em></strong></p>
<ul>
<li><p>Ideal for small, low-complexity projects</p>
</li>
<li><p>Works best for smalled-sized teams (2-3 people max)</p>
</li>
<li><p>Close to no collaborative features, a developer would normally be accustomed to</p>
</li>
<li><p>Always use (and re-use) components whenever possible</p>
</li>
</ul>
<h2 id="heading-testing"><strong>Testing</strong></h2>
<p>We know how much developers love testing code (<strong>*sarcasm*</strong>). Good news: manual testing is pretty much your main 'go-to' approach in the <strong>Power Platform</strong>. For those interested in automated testing, we can use our default framework (<strong>Cypress</strong>), triggered by an external build.</p>
<h2 id="heading-deployment"><strong>Deployment</strong></h2>
<p>When it comes to deployments, we set up a pipeline that can be executed within the <strong>Power Platform</strong>, together with the typical pattern of <em>cross-environment deployment:</em> <strong><em>Dev → Test → Prod</em></strong>.<br />We have noticed that it is sufficient for the before-mentioned, smaller projects.<br />Of course, it is not as powerful as either Gitlab or TeamCity, but that would not be needed.</p>
<h2 id="heading-scale-expand-andamp-maintainability"><strong>Scale, Expand &amp; Maintain...ability</strong></h2>
<p>In terms of scalability, besides maybe investing more money into a <strong>broader user base</strong> or <strong>file storage capabilities</strong>, not much to add in this regard. If the final goal is to create smaller, lower complexity applications, we think the expandability options provided should suffice (an API for CRUDL can be used, as well as Azure Functions with an HTTP trigger, for more complex logic).</p>
<p>Similarly, with the premise of a smaller application, maintainability options offered should again suffice for most projects.</p>
<h2 id="heading-i18n-internationalization"><strong>i18n ("Internationalization")</strong></h2>
<p>i18 support is provided on the platform. There's a reasonable list of available languages that can be activated for every given page, the pattern used is a '<em>key-value</em>'-type approach: <strong><em>language → translation</em></strong>.<br />Except for a few components that are translated automatically (i.e. <em>login</em>), translations must be provided contextually on each page, for each desired language. On the flip side, there's the option of providing a resource file for said translations (that being said, we did not delve into this approach enough to confirm its utility).</p>
<h1 id="heading-lessons-learned"><strong>Lessons Learned</strong></h1>
<p>For some, Microsoft's <strong>Power Platform</strong> might seem to only thrive in specific scenarios (think <em>one-trick pony</em>). We believe that it has earned its place in the ecosystem of modern technology stacks, and when it comes to:</p>
<ul>
<li><p>Rapid Fire MVPs</p>
</li>
<li><p>Purely data-driven projects</p>
</li>
<li><p>"<em>Set it and forget it</em>"-type projects</p>
</li>
<li><p>Pre-defined user bases</p>
</li>
<li><p>Minimal UI</p>
</li>
<li><p>Pre-existing Microsoft ecosystem integration</p>
</li>
<li><p>"<em>Developing without developers</em>" (Welcome to 2023)</p>
</li>
</ul>
<p>there is close to no competition. We say "<em>give it a try</em>".<br />It might end up being the solution you were looking for.</p>
]]></content:encoded></item><item><title><![CDATA[Rapid Development with Strapi and Vue.js]]></title><description><![CDATA[Introduction
As programmers, we often revel in the most technical-complete solutions and prefer writing our services from the ground up. That is both a blessing and a curse: the control, efficiency, and power of writing everything yourself comes at t...]]></description><link>https://engineering.cloudflight.io/rapid-development-with-strapi-and-vuejs</link><guid isPermaLink="true">https://engineering.cloudflight.io/rapid-development-with-strapi-and-vuejs</guid><category><![CDATA[Strapi]]></category><category><![CDATA[Vue.js]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[mvp]]></category><category><![CDATA[mvp development]]></category><dc:creator><![CDATA[Andrei Cotor]]></dc:creator><pubDate>Fri, 15 Sep 2023 11:49:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/dC6Pb2JdAqs/upload/e18e3d1f8cd1bfeb0660b5e152c628d6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>As programmers, we often revel in the most technical-complete solutions and prefer writing our services from the ground up. That is both a blessing and a curse: the control, efficiency, and power of writing everything yourself comes at the cost of speed in development. As part of our Cloudflight technical lab, we decided to explore different solutions that help in delivering software faster - rapid development. This is specifically useful when our teams are asked to deliver a functional MVP in a very short time.</p>
<p><em>If you want to learn more about the challenge please read the</em> <a target="_blank" href="https://engineering.cloudflight.io/navigating-efficient-web-application-development-cloudflights-architectural-insights?source=more_series_bottom_blogs"><em>introduction article</em></a> <em>first.</em></p>
<h2 id="heading-tech-stack">Tech stack</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693492145400/ac4c13f1-b448-40d5-a856-05e3f10e5f74.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-strapi">Strapi</h3>
<p>Strapi is the biggest selling point of our solution. It is a powerful, open-source Node.js and TypeScript Content Management System (CMS). At its core, it is a shortcut for creating REST or GraphQL APIs, replacing the need to write all the backend code yourself. The CMS part, Content Management System, basically means that Strapi provides an admin dashboard running on a web application where you can view your database data, create new API endpoints, and build your server using an intuitive GUI.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693492187500/3a20b417-8316-4a54-8b62-5371905a0e11.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Strapi is the leading open-source headless CMS. It’s 100% Javascript, fully customizable, and developer-first. <a target="_blank" href="https://strapi.io/">Strapi</a></p>
</blockquote>
<p>Diving deeper inside the inner workings of Strapi, we found out that it uses Koa as a web framework and Bookshelf.js, which is powered by Knex, as an Object Relational Mapping (ORM). On top of these two Node.js libraries a "Strapi framework" was built, which can be used by the autogenerated code, or programmatically in case you don't want or can't use the dashboard generator for a specific case. This framework provides generic Controllers, Services to handle CRUD requests, and a generic Entity Service to handle database operations, like find, create, update, and delete. This makes it very easy to customize Strapi, and also for the dashboard to generate the code.</p>
<p>Some nice-to-have feature of the generated code is the possibility to add complex filtering and join operations right in the requests. The format of the parameters gets converted by the Entity Service into a Knex ORM query, which then translates into SQL. Another great thing is out-of-the-box pagination.</p>
<p>Customizing Strapi is generally very facile. Most of the time the only thing that the developers need to do is add some extra functions, inside an object, passed as a parameter to the constructors of the Controllers or Services. You can use them for custom validations or business logic, and they have access to the original Controllers generated for that entity, and all the Services and Entity Service, so developers don't have to reinvent the wheel. For more complex use cases custom endpoints and database queries can be written.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {factories} <span class="hljs-keyword">from</span> <span class="hljs-string">'@strapi/strapi'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> factories.createCoreController(<span class="hljs-string">'api::reservation.reservation'</span>, 
<span class="hljs-function">(<span class="hljs-params">{strapi}</span>) =&gt;</span> ({
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">async</span> find(ctx) {
        <span class="hljs-keyword">const</span> {data, meta} = <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.find(ctx);
        <span class="hljs-comment">// validation for admin users</span>
        <span class="hljs-keyword">if</span> (ctx.state.user.role.name === <span class="hljs-string">'Admin'</span>) {
            <span class="hljs-keyword">return</span> {data, meta};
        }

        <span class="hljs-comment">// custom validation for regular users</span>
        <span class="hljs-keyword">const</span> userId: <span class="hljs-built_in">number</span> = ctx.state.user.id;
        <span class="hljs-keyword">if</span> (data.some(<span class="hljs-function">(<span class="hljs-params">el</span>) =&gt;</span> el.attributes.users_permissions_user.data.id !== 
userId)) {
            <span class="hljs-keyword">return</span> ctx.forbidden(<span class="hljs-string">'Data not created by this user'</span>, {});
        }
        <span class="hljs-keyword">return</span> {data, meta};
    },
    <span class="hljs-comment">// ...</span>
}));
</code></pre>
<blockquote>
<p>This code is essentially creating a controller for a reservation API endpoint, with special handling for admin users and regular users. Admins can access reservation data without restrictions, while regular users can only access data that they have created. If they try to access data created by other users, they receive a "forbidden" response.</p>
</blockquote>
<p>None of us had any experience working with Strapi or anything remotely similar before. As such, we used pair programming in the first week of working together. It proved to be an excellent use case of our time, as we all collaborated in understanding how to work with Strapi. Once we all got used to the overall architecture of our app, we only pair programmed on the important stuff. In the end, we believe it is very easy and intuitive to use, as a couple of days of research were enough to use Strapi to its full potential, as a self-sufficient complex backend service.</p>
<h4 id="heading-plugins">Plugins</h4>
<p>The true strength of Strapi lies in its plugins, which can be added free of charge from Strapi's market (built into the admin dashboard). They add additional functionalities to Strapi, extending its capabilities with no additional code. Some of the most useful plugins we have discovered:</p>
<ul>
<li><p><a target="_blank" href="https://market.strapi.io/plugins/@strapi-plugin-documentation"><em>Documentation</em></a>: generates an OpenAPI document for all of Strapi's endpoints that can either be opened on the browser or be used to generate code down the line. Add this plugin to list all available endpoints and see how to properly make requests to Strapi.</p>
</li>
<li><p><a target="_blank" href="https://market.strapi.io/plugins/strapi-plugin-config-sync"><em>Config Sync</em></a>: allows programmers to share Strapi settings between environments, like access rights to different operations based on roles, either from the CLI or the GUI. This plugin is a must if multiple people are working on the project, or if you want to deploy Strapi.</p>
</li>
</ul>
<p>Other plugins that were preinstalled in our initial Strapi project:</p>
<ul>
<li><p><em>Content Type Builder</em>: Add new data tables in your database from the Strapi GUI. This is what allows you to build new entities and CRUDs. In Strapi, entities can be <em>Collection Types</em> or <em>Single Types</em>. The difference between them is that Single Types only allow for a single value, acting like singletons, while Collection Types are regular tables that support multiple rows.</p>
</li>
<li><p><em>Content Manager</em>: Quick, code-less way to see, edit, and delete the data in your database. This may also allow the owner of the application to manage content without redeployment.</p>
</li>
<li><p><em>Email</em>: Configures the application to send emails. This plugin helps you format your emails and send them further to 3rd party providers.</p>
</li>
<li><p><em>Media Library</em>: Load images and use them in your API - you can easily store multiple image types and use them on your website.</p>
</li>
<li><p><em>Roles &amp; Permissions</em>: JWT-based API security and user management system. This adds the User data type in your application, which you can use for authentication purposes. It supports multiple providers and multiple security roles.</p>
</li>
<li><p><em>Internationalization</em>: Adds the ability to create new locales and set up i18n for your API. Having this plugin allows the owner of the app to localize content without redeployment. The API will return the content for the right locale based on your API request parameters.</p>
</li>
</ul>
<h4 id="heading-performance">Performance</h4>
<p>We load-tested Strapi's create reservation endpoint using Apache JMeter to have a reliable, reproducible result. We used a variable number of users, increasing it over time. We haven't experienced any performance issues so far.</p>
<h3 id="heading-mail-sender">Mail Sender</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694680150722/7978d4f1-116d-43bb-b87c-3faba2e906cd.png" alt class="image--center mx-auto" /></p>
<p>Strapi has a built-in email plugin, but it doesn't automatically send the emails, it just formats them and forwards them to certain providers. There are some providers (node modules) built in, but they are either 3rd party platforms, or they are bare bones (such as <a target="_blank" href="https://nodemailer.com/">nodemailer</a>) so, if you want to handle failures yourself and retry after some time, or you don't want an external service to process your emails, you need to create your own provider. Providers are node modules that extend the functionality of a Strapi plugin.</p>
<p>Since we decided to implement email sending using <a target="_blank" href="https://www.rabbitmq.com/"><strong>RabbitMQ</strong></a> and a microservice, our provider pushes the formatted email that it receives from the Strapi email plugin to the specific RabbitMQ exchange. Then, this exchange sends it to an email queue, where it will be read by our email processing microservice.</p>
<p>To reliably send emails we decided to set up <strong>RabbitMQ</strong> so that we have an email queue, a retry queue, and an error queue. In the email queue, we have the emails that need to be processed by our microservice. If, when trying to send the email an error occurs, the email sender will post the message inside the retry queue if the number of trials to send the specific message is smaller than a set constant. Otherwise, it is going to send it to the error queue. In the retry queue, emails remain for a specific amount of time (they have a time to live), then they get discarded and sent back to the email queue.</p>
<p>This approach guarantees that in most cases our service is going to be able to send the emails, and if not, they will remain in the error queue.</p>
<h3 id="heading-vuejs">VueJS</h3>
<p>All webpages are implemented in VueJS using Composition API. Using our previous experience in Angular and React made VueJS easy to pick up. The official documentation and tutorial were very helpful. To speed up development, we have implemented generic components for all views of our app: a generic form using Vuelidate for validation, a generic table with edit and delete actions, and a generic object details card. Using <a target="_blank" href="https://pinia.vuejs.org/">Pinia</a>, it was also very easy to implement reactive stores.</p>
<p>We decided to use the free version of <a target="_blank" href="https://coreui.io/vue/docs/forms/input.html">CoreUI</a> as the main components library. The date picker component of CoreUI is locked behind a paywall, and we struggled at first to find a reliable date picker component for our reservations view. We settled on using the date picker of <a target="_blank" href="https://primevue.org/">PrimeVue</a>.</p>
<h3 id="heading-openapi-generator">OpenAPI generator</h3>
<p>Most web developers have seen the OpenAPI specification before, as it is what Swagger tools use. Naturally, we decided to create this specification for Strapi. Upon adding the <em>Documentation</em> plugin to our project, we gained access to a .json file containing the full list of our endpoints in OpenAPI specification. This .json file is generated every time we start the Strapi development server. As such, we can open this file using Swagger to view and better understand how to use the APIs provided by Strapi. This plugin allows for customization of the resulting specification file, like the ability to include or exclude properties or endpoints.</p>
<p>Ultimately, we used the OpenAPI specification of our API to also automatically generate REST services for the frontend that can communicate with Strapi. <a target="_blank" href="https://github.com/OpenAPITools/openapi-generator-cli">The tool we used</a> generates Typescript files which we add to our VueJS project. This was the biggest timesaver in our frontend development. Usually, writing the frontend REST services yourself is a redundant task that should be automatized based on your API of choice's specifications.</p>
<h3 id="heading-postgres">Postgres</h3>
<p>PostgreSQL is the official database system recommendation in the setup guide, but multiple database engines are supported. We have used Postgres due to personal preference and convenience.</p>
<h3 id="heading-spring-boot-where-is-it">Spring boot - where is it?</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693491717941/6b494b17-3ec2-4518-9ebf-5249dccf1797.jpeg" alt class="image--center mx-auto" /></p>
<p>The initial tech stack included a self-managed Spring boot application, acting as a man in the middle between the VueJS frontend and the Strapi backend. We removed this layer of complexity in the final solution because Strapi was sufficient enough to secure endpoints and capable enough of adding our app's business logic. Thus, adding Spring would have been redundant and would have just added extra complexity, another possible bottleneck, and would have taken more time to develop. Of course, for larger applications there might be requirements that Strapi might not be suitable for, so a Spring backend might be required as well.</p>
<h2 id="heading-proscons-discussions">Pros/cons discussions</h2>
<p>This is an overview of using Strapi for our backend services:</p>
<h3 id="heading-pros">Pros</h3>
<ul>
<li><p>Facile implementation of CRUDs for models with simple logic</p>
</li>
<li><p>Easy to use: using the GUI for boilerplate code is very convenient</p>
</li>
<li><p>Free to use for any project, if managed on your own premises</p>
</li>
<li><p>Can be extended with custom implementations (you're not limited by the auto-generated code)</p>
</li>
<li><p>Comes with multiple authentication providers out of the box</p>
</li>
<li><p>Offers a lot of plugins, both official and community-made</p>
</li>
<li><p>Multiple SQL database types are supported</p>
</li>
<li><p>Applications may be both vertically and horizontally scaled</p>
</li>
</ul>
<h3 id="heading-cons">Cons</h3>
<ul>
<li><p>Less time-efficient than a self-managed service</p>
</li>
<li><p>Lack of documentation - debugging can be slow</p>
</li>
<li><p>Unexpected crashes for the CMS /admin dashboard while editing the schemas of the collections (not a problem for production - modifying the schemas is disabled in production)</p>
</li>
<li><p>Incomplete migrations support - migrations run before schemas are updated since schemas are managed by Strapi and migrations by Knex</p>
</li>
<li><p>Virtually no validations support - need to do custom endpoints</p>
</li>
<li><p>Does not support MongoDB natively</p>
</li>
</ul>
<h2 id="heading-lessons-learned">Lessons Learned</h2>
<p>There is no tech stack right for every project, but for sure there is a best tool for the job. In our case, considering the functional requirements and the deadline, Strapi did a great job. Using a monolithic architecture and built-in Strapi features was the fastest way for us to deliver the MVP in the given period.  We found out about Strapi that it is a very powerful and versatile way of building backend services and using it was a satisfying experience. It is also both database and frontend agnostic. We definitely will consider it in the future for new projects, either as a stand-alone, self-sufficient server or as a microservice, since it is more than suitable for delivering MVPs or for creating a reliable microservice in larger projects. It is capable of reducing development time and it is quite easy to pick up. However, for the best experience, we recommend installing the plugins of your Strapi project immediately upon setup, to prevent possible merge conflicts and other issues during collaboration with your team members.</p>
<p>Please note that there are also small caveats, like the one that we learned about when we stumbled upon difficulties as we tried to introduce complex logic and validations. For instance, we had 2 entities with a one-to-many relationship between them, but found out that Strapi did not support cascade delete in the default API implementation (see <a target="_blank" href="https://feedback.strapi.io/feature-requests/p/allow-customization-of-foreign-keys-for-options-like-cascade-delete">feature request</a>). As such, we had to add a database-level trigger to delete the ends of the one-to-many relationship.</p>
<p>Another lesson we learned is that Strapi can scale. If you want to scale your application horizontally, you may use a load balancer like Nginx. For additional optimization, you will want to implement caching between sessions. This can be achieved by using the <a target="_blank" href="https://market.strapi.io/plugins/strapi-plugin-rest-cache">Rest Cache</a> Strapi plugin with a self-managed <a target="_blank" href="https://redis.io/">Redis</a> server.</p>
<p>Also, we find VueJS to be a technology worth using in the frontend frameworks ecosystem. Previous experience with other component-based frameworks is a plus, but the official documentation and forums are a great way to start. As an event-driven architecture, it is intuitive to learn and use.</p>
<p><em>Let's roll the credits (in alphabetical order) and wrap up this chapter in our software dev diary: Andrei Cotor, Daniel Todașcă, and Gergely-Péter Mátyás contributed to this exploration. Remember, the code may compile, but the journey is what truly matters. Stay curious, stay coding, and let's keep pushing the boundaries of what's possible, one line of code at a time. Until next time, happy coding, my fellow developers!</em></p>
]]></content:encoded></item></channel></rss>