<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Dan Homola on Medium]]></title>
        <description><![CDATA[Stories by Dan Homola on Medium]]></description>
        <link>https://medium.com/@DanHomola?source=rss-1b1e4f053e40------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*7O62CuqdPHmvWGGbcSAtMw.jpeg</url>
            <title>Stories by Dan Homola on Medium</title>
            <link>https://medium.com/@DanHomola?source=rss-1b1e4f053e40------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 24 Apr 2026 15:04:01 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@DanHomola/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[FlexConnect: Cross-tenant Benchmarking]]></title>
            <link>https://medium.com/gooddata-developers/flexconnect-cross-tenant-benchmarking-0aacb51182b2?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/0aacb51182b2</guid>
            <category><![CDATA[gooddata]]></category>
            <category><![CDATA[benchmarking]]></category>
            <category><![CDATA[data-analysis]]></category>
            <category><![CDATA[business-intelligence]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Thu, 27 Feb 2025 15:55:49 GMT</pubDate>
            <atom:updated>2025-02-27T15:55:49.091Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0DJuOoO7NWNHxZOz" /></figure><p>It is only natural that people and organizations tend to compare themselves to others: it can drive positive change and improvement. For BI solutions that operate on data for data of many (often competing) tenants, it can be a valuable selling point to allow the tenants to compare themselves against others. These can be different businesses, departments in the same business, or even individual teams.</p><p>Since the data of each tenant is often sensitive and proprietary to each tenant, we need to take some extra steps to make the comparison useful without outright releasing the other tenant’s data. In this article, we describe the challenges unique to benchmarking and illustrate how the GoodData FlexConnect data source can be used to overcome them.</p><h3>Benchmarking and its challenges</h3><p>There are two aspects we need to balance when implementing a benchmarking solution:</p><ul><li>Aggregating data across multiple peers</li><li>Picking only relevant peers</li></ul><p>First, we need to aggregate the benchmarking data across multiple peers so that we do not divulge data about any individual peer. We must choose an appropriate granularity (or granularities) on which the aggregation happens. This is very domain-specific, but some common granularities to aggregate peers are:</p><ul><li><strong>Geographic</strong>: same country, continent, etc.</li><li><strong>Industry-based</strong>: same industry</li><li><strong>Aspect-based</strong>: same property (e.g. public vs private companies)</li></ul><p>Second, we need to pick peers that are relevant to the given tenant: comparing to the whole world at once is very rarely useful. Instead, the chosen peers should be in the “same league” as the tenant that is doing the benchmarking. There can also be compliance concerns at play: some tenants can contractually decline to be included in the benchmarks, and so on.</p><p>All of this can make the algorithm to choose the peers very complex: often too complex to implement using traditional BI approaches like SQL. We believe that GoodData FlexConnect is a good choice to implement the benchmarking instead. Using Python to implement arbitrarily complex benchmarking algorithms while plugging seamlessly into GoodData as “just another data source”.</p><h3>What is FlexConnect</h3><p>FlexConnect is a new way of providing data to be used in GoodData. I like to think of it as “code as a data source” because that is essentially what it does — it allows using arbitrary code to generate data and act as a data source in GoodData.</p><p>The contract it needs to implement is quite simple. The FlexConnect gets an execution definition and its job is to return a relevant <a href="https://arrow.apache.org/">Apache Arrow</a> Table. There is our <a href="https://www.gooddata.com/blog/flexconnect-build-your-own-data-source/">FlexConnect Architecture article</a> that goes into much more detail, I highly recommend reading it next.</p><p>For the purpose of this article, we will focus on the code part of the FlexConnect, glossing over the infrastructure side of things.</p><h3>The project</h3><p>To illustrate how FlexConnect can serve benchmarking use cases, we will use the same project available in the GoodData Trial. It consists of one “global” workspace with data for all the tenants and then several tenant-specific workspaces.</p><p>We want to extend this solution with a simple benchmarking capability using FlexConnect so that tenant workspaces can compare themselves to one another.</p><p>More specifically, we will add the capability to benchmark the average amount of returns across the different product categories. We will pick the peers by comparing their total number of orders and will pick those competitors that have a similar number of orders as the tenant running the benchmarking.</p><h3>The solution</h3><p>The solution uses a FlexConnect to select the appropriate peers based on the selected criteria and then runs the same execution against the global workspace with an extra filter making sure that only the peers are used.</p><p>The schema of the data returned by the function makes sure that no individual peer can be visible: there simply is not a column that would hold that information. Let’s dive into the relevant details.</p><h3>The FlexConnect outline</h3><p>The main steps of the FlexConnect is as follows:</p><ol><li>Determine which tenant corresponds to the current user</li><li>Use a custom peer selection algorithm to select appropriate peers to get the comparative data</li><li>Call the global workspace in GoodData to get the aggregate data using the peers from the previous step</li></ol><p>The FlexConnect returns data conforming to the following schema:</p><pre>import pyarrow<br><br>Schema = pyarrow.schema(<br>    [<br>        pyarrow.field(&quot;wdf__product_category&quot;, pyarrow.string()),<br>        pyarrow.field(&quot;mean_number_of_returns&quot;, pyarrow.float64()),<br>    ]<br>)</pre><p>As you can see, the schema returns a benchmarking metric sliced by individual product categories. This gives us very strict control about which granularities of the benchmarking data we want to allow: there is no way a particular competitor would leak here.</p><h3>Current tenant detection</h3><p>First, we need to determine which tenant is the one we are choosing the peers for. Thankfully, each FlexConnect invocation receives the information about which workspace it is being called from. We can use this to map the workspace to the tenant it corresponds to.</p><p>For simplicity’s sake, we use a simple lookup table in the FlexConnect itself, but this logic can be as complex as necessary — in real life scenarios, this is often stored in some data warehouse and you could query for this information (and possibly caching it).</p><pre>import gooddata_flight_server as gf<br><br>TENANT_LOOKUP = {<br>    &quot;gdc_demo_..1&quot;: &quot;merchant__bigboxretailer&quot;,<br>    &quot;gdc_demo_..2&quot;: &quot;merchant__clothing&quot;,<br>    &quot;gdc_demo_..3&quot;: &quot;merchant__electronics&quot;,<br>}<br><br># This is the API you need to implement, it has to adhere to the PyArrow schema<br>def call(<br>    self,<br>    parameters: dict,<br>    columns: Optional[tuple[str, ...]],<br>    headers: dict[str, list[str]],<br>) -&gt; gf.ArrowData:<br>    execution_context = ExecutionContext.from_parameters(parameters)<br>    tenant = TENANT_LOOKUP.get(execution_context.workspace_id)<br>    peers = self._get_peers(tenant)<br>    return self._get_benchmark_data(<br>        peers, execution_context.report_execution_request<br>    )</pre><h3>Peer selection</h3><p>With the current tenant known, we can then select the peers for the benchmarking. We use a custom SQL query, which we run against the source database. This query selects peers that have similar values in the number of orders (we consider competitors that have 80–200% the amount of our order quantity). Since the underlying database is Snowflake, we use the Snowflake-specific syntax to inject the current tenant into the query.</p><p>Please keep in mind that the fact we use SQL here is meant to illustrate that the peer selection can use any algorithm you want and can be as complex as needed based on business or compliance needs. E.g., it could contact <a href="https://www.gooddata.com/blog/flexconnect-integrate-any-apis-into-bi/">some external API.</a></p><pre>import os<br><br>import snowflake.connector<br><br><br>def _get_connection(self) -&gt; snowflake.connector.SnowflakeConnection:<br>    ...  # omitted for brevity<br><br>def _get_peers(self, tenant: str) -&gt; list[str]:<br>    &quot;&quot;&quot;<br>    Get the peers that have comparable number of orders to the given tenant.<br>    :param tenant: the tenant for which to find peers<br>    :return: list of peers<br>    &quot;&quot;&quot;<br>    with self._get_connection() as conn:<br>        cursor = conn.cursor()<br>        cursor.execute(<br>            &quot;&quot;&quot;<br>        WITH PEER_STATS AS (<br>            SELECT COUNT(*) AS total_orders,<br>                   &quot;wdf__client_id&quot; AS client_id,<br>                   IFF(&quot;wdf__client_id&quot; = %s, &#39;current&#39;, &#39;others&#39;) AS client_type<br>            FROM TIGER.ECOMMERCE_DEMO_DIRECT.&quot;order_lines&quot;<br>            GROUP BY &quot;wdf__client_id&quot;, client_type<br>        ),<br>        RELEVANT_PEERS AS (<br>            SELECT DISTINCT others.client_id<br>            FROM PEER_STATS others CROSS JOIN PEER_STATS curr<br>            WHERE curr.client_type = &#39;current&#39;<br>              AND others.client_type = &#39;others&#39;<br>              AND curr.total_orders BETWEEN others.total_orders * 0.8 AND others.total_orders * 2<br>        )<br>        SELECT * FROM RELEVANT_PEERS<br>            &quot;&quot;&quot;,<br>            (tenant,),<br>        )<br><br>        record = cursor.fetchall()<br>        return [row[0] for row in record]</pre><h3>Benchmarking data computation</h3><p>Once we have the peers ready, we can query the global GoodData workspace for the benchmarking data. We can take advantage of the fact that we get the information about the original execution definition passed to the FlexConnect when invoked.</p><p>This allows us to keep any filters applied to the report: without this, the benchmarking data would be filtered differently, rendering it meaningless. The relevant part of the code looks like this:</p><pre>import os<br><br>import pyarrow<br>from gooddata_flexfun import ReportExecutionRequest<br>from gooddata_pandas import GoodPandas<br>from gooddata_sdk import (<br>    Attribute,<br>    ExecutionDefinition,<br>    ObjId,<br>    PositiveAttributeFilter,<br>    SimpleMetric,<br>    TableDimension,<br>)<br><br>GLOBAL_WS = &quot;gdc_demo_...&quot;<br><br>def _get_benchmark_data(<br>    self, peers: list[str], report_execution_request: ReportExecutionRequest<br>) -&gt; pyarrow.Table:<br>    # GoodPandas = GoodData -&gt; Pandas<br>    pandas = GoodPandas(os.getenv(&quot;GOODDATA_HOST&quot;), os.getenv(&quot;GOODDATA_TOKEN&quot;))<br><br>    (frame, metadata) = pandas.data_frames(GLOBAL_WS).for_exec_def(<br>        ExecutionDefinition(<br>            attributes=[Attribute(&quot;product_category&quot;, &quot;product_category&quot;)],<br>            metrics=[<br>                SimpleMetric(<br>                    &quot;return_unit_quantity&quot;,<br>                    ObjId(&quot;return_unit_quantity&quot;, &quot;fact&quot;),<br>                    &quot;avg&quot;,<br>                )<br>            ],<br>            filters=[<br>                *report_execution_request.filters,<br>                # add a filter limiting the scope to the peers<br>                PositiveAttributeFilter(ObjId(&quot;client_id&quot;, &quot;label&quot;), peers),<br>            ],<br>            dimensions=[<br>                TableDimension([&quot;product_category&quot;]),<br>                TableDimension([&quot;measureGroup&quot;]),<br>            ],<br>        )<br>    )<br><br>    frame = frame.reset_index()<br>    frame.columns = [&quot;wdf__product_category&quot;, &quot;mean_number_of_returns&quot;]<br><br>    return pyarrow.Table.from_pandas(frame, schema=self.Schema)</pre><h3>Changes to LDM</h3><p>Once the FlexConnect is running somewhere reachable from GoodData (e.g., AWS Lambda), we can connect the FlexConnect as a data source.</p><p>To be able to connect the dataset from it to the rest of the logical data model, we need to make two changes to the existing model first:</p><ul><li>Promote <em>product category</em> to a standalone dataset</li><li>Apply the WDF that exists on the <em>product category</em> to new and benchmarking datasets</li></ul><p>Since our benchmarking function is sliceable by product category, we need to promote product category to a stand alone dataset. This will allow it to act as a bridge between the benchmarking dataset and the rest of the data.</p><p>We need to apply the WDF that exists on the product category in the model to both the new and the benchmarking datasets. This ensures the benchmark will not leak product categories available to some of the peers but not to the current tenant. This also shows how seamlessly the FlexConnects fit into the rest of GoodData: we treat them the same way we would treat any other dataset.</p><p>Let’s have a look at the before and after screenshots of the relevant part of the logical data model.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/534/0*ALdffQWYiLSqvPT_" /><figcaption>LDM before the changes</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*oHhN30TYqpAdzWYx" /><figcaption><em>LDM after the changes</em></figcaption></figure><h3>In Action</h3><p>With these changes in place, we can finally use the benchmark in our analytics! Below is an example of a simple table comparing the returns of a given tenant to its peers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BIyXYjFKuNPKy0uT" /><figcaption><em>Example benchmarking insight</em></figcaption></figure><p>In this particular insight, the tenant sees that their returns for Home Goods are a bit higher than those of their peers, so maybe there is something to be investigated there.</p><p>There is no data for some of the product categories, but that is to be expected: sometimes there are no relevant peers for a given category, so it is completely fine that the benchmark returns nothing for it.</p><h3>Summary</h3><p>Benchmarking is a deceptively complicated problem: we must balance the usefulness of the values with compliance to the confidentiality principles. This can prove to be quite hard to implement in traditional data sources. We have outlined a solution based on FlexConnect that offers much greater flexibility both in the peer selection process and the aggregated data computation.</p><h3>Want to Learn More?</h3><p>If you want to learn more about GoodData FlexConnect, I highly recommend you read the aforementioned <a href="https://www.gooddata.com/blog/flexconnect-build-your-own-data-source/">architectural article</a>.</p><p>If you’d like to see more use-cases for FlexConnect, check out our <a href="https://www.gooddata.com/blog/flexconnect-ml-in-bi-made-easy/">machine learning</a> or <a href="https://www.gooddata.com/blog/flexconnect-intagrate-nosql-into-bi/">NoSQL articles</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0aacb51182b2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gooddata-developers/flexconnect-cross-tenant-benchmarking-0aacb51182b2">FlexConnect: Cross-tenant Benchmarking</a> was originally published in <a href="https://medium.com/gooddata-developers">GoodData Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[FlexConnect: Integrate NoSQL Into BI]]></title>
            <link>https://medium.com/gooddata-developers/flexconnect-integrate-nosql-into-bi-cfe50a5d25ff?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/cfe50a5d25ff</guid>
            <category><![CDATA[nosql]]></category>
            <category><![CDATA[business-intelligence]]></category>
            <category><![CDATA[mongodb]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Wed, 04 Dec 2024 10:35:13 GMT</pubDate>
            <atom:updated>2024-12-04T10:35:13.205Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HqoiAGt7-cDuBOMt" /></figure><p>Over the past several years, NoSQL databases (and document-oriented databases specifically) have seen significant adoption in fields like IoT and E-Commerce. They provide some interesting advantages over more traditional SQL-based databases, including <strong>flexibility, scalability, and ease of use for developers</strong>. However, while flexibility is a benefit, it can make it challenging to connect these databases to analytics platforms.</p><p>In this article, we show how FlexConnect, a new GoodData feature, can be used to connect a MongoDB collection to GoodData using a little Python code. We demonstrate this using the MongoDB <a href="https://www.mongodb.com/docs/atlas/sample-data/sample-mflix">Sample Mflix</a> demo data about movies — so that it is easy to follow. We also take advantage of MongoDB client supporting the Apache Arrow format to achieve much simpler and more efficient integration.</p><p><em>This article is part of the FlexConnect Launch Series. To dive deeper into the overall concept, be sure to check out our </em><a href="https://medium.com/gooddata-developers/flexconnect-build-your-own-data-source-f4f0c8816bea"><em>architectural overview</em></a><em>. Don’t miss our other articles on topics such as </em><a href="https://medium.com/gooddata-developers/flexconnect-integrate-any-apis-into-bi-b0b9b1e7a230"><em>API ingestion</em></a><em>, NoSQL integration, </em><a href="https://medium.com/gooddata-developers/flexconnect-bridging-kafka-and-bi-10729dfc6384"><em>Kafka connectivity</em></a><em>, </em><a href="https://medium.com/gooddata-developers/flexconnect-ml-in-bi-made-easy-b0067f9b2317"><em>Machine Learning</em></a><em>, and </em><a href="https://medium.com/gooddata-developers/flexconnect-integrating-unity-catalog-into-your-bi-df0d932a2c4e"><em>Unity Catalog</em></a><em>!</em></p><h4>Challenges</h4><p>Unlike most SQL-based databases, document databases can store data with complex structures, including nested objects, lists, and more. The schema of the documents is also not as rigid as in the SQL world. These factors make it quite challenging to fit the data into a <a href="https://en.wikipedia.org/wiki/Star_schema">Star-</a> or <a href="https://en.wikipedia.org/wiki/Snowflake_schema">Snowflake schema</a>, commonly used in BI to provide self-service capabilities to end users. Any solution that aims to connect those two worlds needs to be flexible enough to allow for customization of the mapping logic. This is essential to cope with the dynamic and loosely-structured nature of the documents.</p><h3>What Is FlexConnect?</h3><p>FlexConnect is a new way of providing data to be used in GoodData. It enables connections to virtually any data source while integrating with the rest of GoodData, just like any natively supported data source type.</p><p>I like to think of it as “code as a data source,” because that is essentially what it does: it allows the use of arbitrary code to generate data and act as a data source in GoodData. The contract it needs to implement is quite simple: based on the execution definition, return the relevant data as an Apache Arrow Table over the Arrow Flight RPC protocol (I highly recommend our detailed <a href="https://medium.com/gooddata-developers/arrow-flight-rpc-101-583517ad1539">primer</a> on Arrow Flight RPC). FlexConnect is then deployed anywhere you like — as long as GoodData can reach it and it acts as “just another data source.” This article goes into much more detail, so I highly recommend reading it next. For the purpose of this article, we will focus on the code part of FlexConnect, glossing over the infrastructure side of things.</p><h3>FlexConnect for Movie Collection</h3><p>To showcase FlexConnect’s capabilities, we will use MongoDB, a popular choice among document-oriented databases, as a representative. We will use their <a href="https://www.mongodb.com/docs/atlas/sample-data/sample-mflix">Sample Mflix</a> dataset to create a simple FlexConnect implementation exposing some of the data to GoodData.</p><p>To get started, we clone the <a href="https://github.com/gooddata/gooddata-flexconnect-template">gooddata-flexconnect-template repository</a>. This bootstraps the necessary boilerplate we need to get started. It is a production-ready setup based on our <a href="https://pypi.org/project/gooddata-flight-server/">gooddata-flight-server</a> package that handles the necessary infrastructure for exposing data using the Apache Arrow Flight RPC protocol, the basis of FlexConnect.</p><h4>Dependencies</h4><p>Once that is done, we can start implementing the MongoDB connector. MongoDB provides a Python client for easy interaction with the databases. They also provide a plugin of sorts that adds some Apache Arrow-friendly APIs. We will be using both, so we need to add those to our project:</p><ul><li><a href="https://pypi.org/project/pymongo/">pymongo</a> — the base client</li><li><a href="https://pypi.org/project/pymongoarrow/">pymongoarrow</a> — the Apache Arrow “plugin”</li></ul><h4><strong>Schemas</strong></h4><p>Next, let’s define the data schema we will expose to GoodData and the schema of the data we will pull from MongoDB. Thanks to pymongoarrow, we can define both at once!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xpRO9YoU1NInMRnZ" /><figcaption>Schema of the mapping of the JSON on GoodData table</figcaption></figure><pre>import pyarrow<br>from pymongoarrow.api import Schema as MongoSchema<br><br># This is the schema the data returned by the MongoDB query will have.<br>DbSchema = MongoSchema(<br>    {<br>        &quot;title&quot;: pyarrow.string(),<br>        &quot;rated&quot;: pyarrow.string(),<br>        &quot;released&quot;: pyarrow.timestamp(&quot;ms&quot;),<br>        &quot;critic_rating&quot;: pyarrow.int64(),<br>        &quot;viewer_rating&quot;: pyarrow.int64(),<br>    }<br>)<br><br># We need to advertise the schema of the data we are going to send to GoodData.<br># This is part of the FlexConnect function contract.<br>Schema = DbSchema.to_arrow()</pre><p>As you can see in the code snippet, we define the schema of the results we want to get from MongoDB and then expose it as a pure Apache Arrow Schema object to GoodData. This ensures that we will not need to convert the results from MongoDB to GoodData-compatible data; we will just pass them along (more details to follow).</p><h4>Report execution code</h4><p>Now, we will focus on the part where FlexConnect handles the report execution requests. These correspond to a user opening a report and requesting data for it. First, to make things leaner, we define a simple contextmanager that exposes the “movies” collection and ensures the connection is closed appropriately.</p><pre>import os<br>from collections.abc import Generator<br>from contextlib import contextmanager<br><br>from pymongo import MongoClient<br>from pymongo.synchronous.collection import Collection<br><br>CONNECTION_STRING = os.getenv(&quot;MONGO_CONN_STRING&quot;)<br><br>@staticmethod<br>@contextmanager<br>def _get_movie_collection() -&gt; Generator[Collection, None, None]:<br>    &quot;&quot;&quot;<br>    Get the MongoDB collection with movies and make sure it is closed after use.<br>    &quot;&quot;&quot;<br>    client = MongoClient(CONNECTION_STRING)<br>    try:<br>        db = client.get_database(&quot;sample_mflix&quot;)<br>        yield db.get_collection(&quot;movies&quot;)<br>    finally:<br>        client.close()</pre><p>Next, we move to the code for the report execution. We connect to the MongoDB database and query it for the necessary information. We use the DbSchema defined earlier to make MongoDB return the data directly in the format we want to pass to GoodData (remember, we made those two schemas identical for this very reason).</p><pre>import gooddata_flight_server as gf<br>from gooddata_flexconnect import ExecutionContext, ExecutionType<br><br><br>def call(<br>    self,<br>    parameters: dict,<br>    columns: Optional[tuple[str, ...]],<br>    headers: dict[str, list[str]],<br>) -&gt; gf.ArrowData:<br>    execution_context = ExecutionContext.from_parameters(parameters)<br>    if execution_context.execution_type == ExecutionType.REPORT:<br>        with self._get_movie_collection() as collection:<br>            return collection.find_arrow_all(<br>                # We can pass the filters directly to the find_arrow_all method<br>                # to optimize the query and avoid unnecessary data transfer.<br>                query=self._report_filters_to_mongo_query(<br>                    execution_context.report_execution_request.filters,<br>                    execution_context.timestamp,<br>                ),<br>                projection={<br>                    # We can project fields as they are in the MongoDB collection<br>                    &quot;title&quot;: &quot;$title&quot;,<br>                    &quot;rated&quot;: &quot;$rated&quot;,<br>                    &quot;released&quot;: &quot;$released&quot;,<br>                    # We can project nested fields as well<br>                    &quot;critic_rating&quot;: &quot;$tomatoes.critic.meter&quot;,<br>                    &quot;viewer_rating&quot;: &quot;$tomatoes.viewer.meter&quot;,<br>                },<br>                schema=self.DbSchema,<br>            )<br>    elif execution_context.execution_type == ExecutionType.LABEL_ELEMENTS:<br>        ... # will be discussed in the next snippet</pre><p>As you can see, we simply define the appropriate projections to get the data from the correct locations in the documents and return the data directly. The pymongoarrow will convert the results from the database to an Apache Arrow Table of the desired shape under the hood.</p><p>You may have noticed that we also define the query to limit the number of results based on the filters from the execution request. While this is optional, it is a good practice as it can make things significantly faster instead of returning data from all the documents all the time. This will be discussed later in a separate chapter. The important thing to note here, though, is that even if you do not filter the data, <strong>GoodData will still ensure all the execution filters are applied correctly</strong>, so filtering here is mainly a means of optimizing things.</p><h4>Label elements code</h4><p>The other type of execution we need to handle is requests for label elements. For example, all the different ratings of all of the movies. These are primarily used to populate the UI label filter pickers. Basically, we get a label’s id, and we should return all the labels’ distinct values in our data.</p><pre>import gooddata_flight_server as gf<br>import pyarrow<br>from gooddata_flexconnect import ExecutionContext, ExecutionType<br><br><br>def call(<br>    self,<br>    parameters: dict,<br>    columns: Optional[tuple[str, ...]],<br>    headers: dict[str, list[str]],<br>) -&gt; gf.ArrowData:<br>    execution_context = ExecutionContext.from_parameters(parameters)<br>    if execution_context.execution_type == ExecutionType.REPORT:<br>        ... # see the previous snippet for the implementation<br>    elif execution_context.execution_type == ExecutionType.LABEL_ELEMENTS:<br>        with self._get_movie_collection() as collection:<br>            # Get the label we want to get elements for.<br>            # No need for mapping here: the label has the same name as the field in the MongoDB collection.<br>            label = execution_context.label_elements_execution_request.label<br>            # We can use the distinct method to get unique values of a field.<br>            # There is unfortunately no Arrow-native way to do this, so we need to convert the result to a table.<br>            elems = collection.distinct(<br>                key=label,<br>                filter=self._elements_request_to_mongo_query(<br>                    execution_context.label_elements_execution_request<br>                ),<br>            )<br>            # Add None to the list of elements to represent the null value:<br>            # this will not be returned by the distinct method because it is a part of an index.<br>            elems.append(None)<br>            # We need to return a table with a single column with the label elements.<br>            # This needs to be a subset of the schema we advertise to GoodData.<br>            return pyarrow.Table.from_pydict(<br>                {label: elems},<br>                schema=pyarrow.schema({label: pyarrow.string()}),<br>            )</pre><p>This time, we cannot use a pymongoarrow API as there seems to be no counterpart to the distinct operation, so we need to convert the results to an Apache Arrow Table manually. Taking advantage of the fact that all the labels in our dataset are strings, we can construct the simple one-column schema easily.</p><p>Similar to the report execution, you can see we use a filter expression to limit the data transferred by reflecting the user filters on the returned elements (typically what they wrote in a search box). This will be discussed later, but as in the report execution case, even if you skip this, GoodData will apply all the filters anyway.</p><h3>In action within GoodData</h3><p>Once the code is written and FlexConnect is deployed, we can connect it to GoodData and use it for analytics. Using either the UI or the API, we can <a href="https://www.gooddata.com/docs/cloud/connect-data/create-data-sources/flexconnect/">create a new data source</a> of the FLEXCONNECT data source type. Then, using the Logical Data Modeler, we can add our FlexConnect function dataset to the LDM.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/606/0*KlMAj5joQr4wBTg5" /><figcaption>View of the Logical Data Model</figcaption></figure><p>And that is it. Now, we can use the Analytical Designer to create some interesting reports. For example, we can investigate how much the viewer and critic ratings differ based on the release month among PG-13-rated movies in the ‘90s.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2tQZDMkmlP0Uxn_o" /><figcaption>Sample report using the FlexConnect data from MongoDB</figcaption></figure><h3>Optimizing the data transfers</h3><p>This section covers the additional steps you can take to make your FlexConnect function more efficient. This is optional, but beneficial. We will cover how to limit the data transferred during the MongoDB communication by leveraging the filters specified in the execution definitions. Keep in mind that the optimizations do not have to be perfect, you can take an iterative approach to them, and GoodData will still ensure all the data is displayed correctly every time.</p><h4>Label elements</h4><p>For label elements, we can keep things simple by respecting the search query the user typed into the search box. This can significantly reduce the data transferred, especially for labels that have many different elements.</p><p>We will show you only some of the optimizations here. For complete code and examples of these optimizations, check out our <a href="https://github.com/Mara3l/article-demos/tree/main/flexconnect/mongodb">GitHub repository</a>. This will help you dive deeper into the implementation details and adapt the techniques to your use case.</p><pre>from gooddata_flexconnect import LabelElementsExecutionRequest<br><br><br>@staticmethod<br>def _elements_request_to_mongo_query(<br>    request: LabelElementsExecutionRequest,<br>) -&gt; dict[str, dict]:<br>    &quot;&quot;&quot;<br>    Convert GoodData label elements request to MongoDB query.<br>    &quot;&quot;&quot;<br>    query = {}<br>    if request.pattern_filter:<br>        query[request.label] = {<br>            &quot;$regex&quot;: request.pattern_filter,<br>            &quot;$options&quot;: &quot;i&quot;, # the search should be case-insensitive<br>        }<br>    return query</pre><h4>Report execution</h4><p>For report executions, the filter variability is a bit greater as there are quite a few filter types. However, we can only focus on the ones we expect to be used the most or those that can have the biggest impact.</p><p>The <a href="https://github.com/gooddata/gooddata-article-demos/blob/master/flexconnect/mongodb/src/flexconnect/mongo_flex_connect.py#L152">snippet in the GitHub repo</a> shows how to reflect attribute and date filters in the MongoDB queries for report executions. We use the standard MongoDB <a href="https://www.mongodb.com/docs/manual/reference/operator/query/">query operators</a> and fill them with the appropriate values.</p><h3>Do More With FlexConnect</h3><p>Bridging the gap between the semi-structured world of document databases and the structured world of BI tools is not a trivial task. In this article we have shown that using GoodData FlexConnect, doing so is straightforward with a bit of Python code. It offers great flexibility and is easy to get started with.</p><h3>Learn More</h3><p>FlexConnect is built for developers and companies looking to streamline the integration of diverse data sources into their BI workflows. It gives you the flexibility and control you need to get the job done with ease.</p><p>Explore detailed use cases like <a href="https://medium.com/gooddata-developers/flexconnect-integrate-any-apis-into-bi-b0b9b1e7a230">connecting APIs</a>, <a href="https://medium.com/gooddata-developers/flexconnect-ml-in-bi-made-easy-b0067f9b2317">running local machine learning models</a>, <a href="https://medium.com/gooddata-developers/flexconnect-integrate-nosql-to-bi-cfe50a5d25ff">handling semi-structured NoSQL data</a>, <a href="https://medium.com/@jkadlec/flexconnect-bridging-kafka-and-bi-10729dfc6384">streaming real-time data from Kafka</a>, or <a href="https://medium.com/gooddata-developers/flexconnect-integrating-unity-catalog-into-your-bi-df0d932a2c4e">integrating with Unity Catalog</a> — each with its own step-by-step guide.</p><p>Want the bigger picture? Check out our <a href="https://medium.com/gooddata-developers/flexconnect-build-your-own-data-source-f4f0c8816bea">architecture article</a> on FlexConnect, or connect with us through our <a href="https://gooddata.com/slack">Slack community</a> for support and discussion.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cfe50a5d25ff" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gooddata-developers/flexconnect-integrate-nosql-into-bi-cfe50a5d25ff">FlexConnect: Integrate NoSQL Into BI</a> was originally published in <a href="https://medium.com/gooddata-developers">GoodData Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[CSV Files in Analytics: Taming the Variability]]></title>
            <link>https://medium.com/gooddata-developers/csv-files-in-analytics-taming-the-variability-34ee7fa74754?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/34ee7fa74754</guid>
            <category><![CDATA[data]]></category>
            <category><![CDATA[analytics]]></category>
            <category><![CDATA[csv]]></category>
            <category><![CDATA[business-intelligence]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Tue, 16 Apr 2024 20:03:00 GMT</pubDate>
            <atom:updated>2024-06-05T09:59:13.020Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*B5SpyF1HLczosrES" /></figure><p>There are few formats as ubiquitous as CSV: most applications for which it makes even a smidge of sense to do so, support storing their output as CSV. Apart from its popularity, the format itself has quite a few additional advantages:</p><ul><li>It is <strong>human-readable</strong>, making it easy to create, read, and edit in any text editor, over the terminal, etc.</li><li>It is <strong>approachable</strong> even for non-technical users, they can make sense of the raw contents easily.</li><li>It can be <strong>versioned</strong> using Git and other version control systems.</li><li>It is relatively <strong>condensed</strong> compared to other text-based formats like XML or JSON.</li></ul><p>As with all things, CSV has its downsides, too:</p><ul><li>It is <strong>less efficient</strong> to store. For example, numbers take up space for each digit instead of being stored as a number.</li><li>Being <strong>row-based</strong>, it is quite hard to get data only for certain columns: the whole file needs to be read even if we care about the first two columns, for example.</li><li>There is <strong>no one universal CSV standard</strong>, it has several variants or dialects.</li></ul><p>When adding support for CSV files into <a href="https://medium.com/gooddata-developers/project-longbow-35f5a860bb9b">Longbow</a> — our framework for creating modular data services — it was the last point that was especially challenging. In this article, we describe the approach we took with it.</p><h3>What information do we need to extract</h3><p>Let’s discuss what aspects of the CSV files we need to concern ourselves with when ingesting them into Longbow for further use. For each file, we need to derive the following:</p><ol><li>The <strong>encoding</strong> used by the file (ASCII, UTF-8, etc.).</li><li><strong>Dialect</strong> used by the file (delimiters, quotes, etc.).</li><li>Names of the <strong>columns</strong>.</li><li><strong>Types</strong> of the data in the columns (integer, string, date, etc.).</li><li><strong>Preview</strong> of the first several rows so that the user can verify the CSV was parsed correctly.</li></ol><p>We will explore the steps we took for each of these items in more detail in the rest of the article.</p><h3>The ingest process</h3><p>Before diving into the individual steps, let’s take a look at what the process of adding a new file looks like. First, the user uploads the CSV file they want to use to what we call a <strong>staging area</strong>. This is so we can run some analysis on the file using Longbow and show the results to the user. The user can review that the file is parsed correctly, and they can tweak some of the settings. Then, if they are satisfied with the results, they can proceed with confirming the file import. Once they do that, the file is moved from the staging area to the <strong>production area</strong> and it is then ready for use.</p><h3>Storing the metadata</h3><p>CSV has no dedicated way of storing any kind of metadata in the file itself (apart from somehow including it before the actual data), and we also want to support read-only input files. We had to devise a mechanism to store the metadata detected in the steps described below somewhere. We ended up with dedicated manifest files. The manifests are located right next to the relevant CSV files and have the same name with the .manifest suffix. They contain JSON-serialized versions of all the configurations we have collected both from the analysis and the user. Every time a particular CSV file is requested, we first check the manifest and use the configuration stored there to read the actual CSV file.</p><p>The configuration itself consists of options accepted by the <a href="https://arrow.apache.org/docs/python/csv.html">Arrow CSV module</a> (ReadOptions, ParseOptions, and ConvertOptions) that are used as-is when reading the CSV file. We also store information about date formats for any columns that should be interpreted as dates (more on that later).</p><h3>Detecting the encoding</h3><p>The very first step when reading an unknown CSV file (or any text file for that matter) for any analysis is to determine the encoding used by the file. This is to avoid any surprises with non-UTF-8 files being interpreted the wrong way. We use the <a href="https://charset-normalizer.readthedocs.io/en/latest/">charset_normalizer</a> package for this purpose. The detected encoding is then used in subsequent reads of the CSV file.</p><h3>Detecting the dialect and column names</h3><p>The next step is to detect the so-called dialect of the CSV file. The dialect describes some of the structural properties of the CSV:</p><ul><li>What is the <strong>separating character</strong> for the individual columns?</li><li>Are there any <strong>quotation marks</strong> used to escape the separators, and if so, how can they be escaped?</li></ul><p>We also need to detect the column names. Some CSV files store the column names in the first row, some do not store them at all, and we need to generate some ourselves.</p><p>We use DuckDB’s <a href="https://duckdb.org/docs/data/csv/auto_detection">sniff_csv function </a>to gather all of this information. It gives us all the structural information about the file, like the delimiters, quotes, etc. It also detects the column headers if there are any, falling back on autogenerated column names. You can read more about the DuckDB CSV capabilities in their introductory <a href="https://duckdb.org/2023/10/27/csv-sniffer.html">blog post</a>. We also need to make sure that the file we feed into DuckDB is in <strong>UTF-8</strong>. Otherwise, it fails. We make use of the detected encoding and prepare a special copy of the input file just for DuckDB in case the original is not in UTF-8 (or ASCII).</p><pre>def _detect_dialect_and_header_and_column_names(<br>    sample_filename: str,<br>    encoding: str,<br>) -&gt; tuple[CsvDialect, int, list[str]]:<br>    needs_conversion = encoding not in [&quot;utf_8&quot;, &quot;ascii&quot;]<br>    if needs_conversion:<br>        duckdb_input_file = sample_filename + &quot;.utf_8.csv&quot;<br>        # ... convert the sample file to utf-8<br>    else:<br>        duckdb_input_file = sample_filename<br><br>    try:<br>        return _run_duckdb_detection(duckdb_input_file)<br>    finally:<br>        if needs_conversion:<br>            os.unlink(duckdb_input_file)<br><br>def _run_duckdb_detection(<br>    duckdb_input_file: str,<br>) -&gt; tuple[CsvDialect, int, list[str]]:<br>    # use only one thread, we will always run only one query at a time from this<br>    conn = duckdb.connect(&quot;:memory:&quot;, config={&quot;threads&quot;: 1})<br>    query = conn.execute(<br>        &quot;SELECT Delimiter, Quote, Escape, HasHeader, Columns FROM sniff_csv(?)&quot;,<br>        [duckdb_input_file],<br>    )<br>    query_result = query.fetchone()<br>    if not query_result:<br>        raise ValueError(&quot;Unable to detect file dialect.&quot;)<br><br>    (delimiter, quote, escape, has_header, columns) = query_result<br>    # the columns are returned as a list of dictionaries, we need to extract the names<br>    column_names = [col[&quot;name&quot;] for col in columns]<br><br>    # the detection may return \x00 as a delimiter, need to normalize to None<br>    dialect = CsvDialect(<br>        delimiter=delimiter if delimiter != &quot;\x00&quot; else None,<br>        quotechar=quote if quote != &quot;\x00&quot; else None,<br>        escapechar=escape if escape != &quot;\x00&quot; else None,<br>    )<br><br>    if has_header:<br>        # the header takes as many rows as there are newlines in the column names<br>        # plus one for the first line itself<br>        header_row_count = sum(col_name.count(&quot;\n&quot;) for col_name in column_names) + 1<br>    else:<br>        header_row_count = 0<br><br>    return dialect, header_row_count, column_names</pre><p><em>Changed on 23rd of May 2024: as of 0.10.3, duckdb now has better support for the </em><em>Columns output of the </em><em>sniff_csv function that allows us to simplify the column name parsing.</em></p><p>Before the sniff_csv was available, we used the <a href="https://clevercsv.readthedocs.io/en/latest/index.html">CleverCSV</a> library for this step. Still, the DuckDB variant performs better (we observed a ten-fold improvement in the overall time) and allowed us to simplify the code since it can detect the dialect and column names in one step.</p><h3>Detecting the data types</h3><p>Having a way to read the file with the schema in hand, we can proceed with determining the actual data type of each column. You might ask, “Why not use the types detected by DuckDB?” or “Why not use the automatic detection that Arrow CSV has?”. There are a few reasons, but the most significant one has to do with the various date formats we want to support. The DuckDB CSV sniffer only supports one date format per file, so if you use one date format in one column and another format in another column, it will not work. Arrow CSV does support different date formats per column, but the set of date formats it supports is limited. While it would work great with <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> compliant dates, for example, it would not recognize strings like:</p><ul><li><strong>Jan 22, 2023 01:02:03</strong></li><li><strong>01 22 23 01:02:03</strong></li><li><strong>20230122</strong></li></ul><p>as potentially being dates as well. This is not to say the Arrow detection is wrong (after all, the last example may very well be just an integer). We just need to support a wider set of formats.</p><p>You can <a href="https://arrow.apache.org/docs/python/generated/pyarrow.csv.ConvertOptions.html#pyarrow.csv.ConvertOptions.timestamp_parsers">specify</a> which date formats you want Arrow to try, but in case of ambiguity, it will always assume that the first matching format is correct. We want our users to disambiguate the date format manually: only they know which format is the correct one.</p><p>Another limitation of the Arrow CSV approach is that you either get the most precise data type detection (but you need to read the whole file into memory -which obviously does not scale that well), or you can use the batch-based approach. Still, only <a href="https://arrow.apache.org/docs/python/csv.html#incremental-reading">the first batch of the file is used</a> for the data type detection making it less precise.</p><p>We want the most precise detection while conserving the memory. To that end, our pipeline is constructed a bit differently. First, we tell Arrow to read the file batch by batch and to treat each column as a string so that we avoid any automatic detection performed by Arrow. This is where the column names come in handy: you need their names to reference them in the Arrow CSV options. Next, we pipe this source into a custom Acero pipeline that allows us to run the analysis extremely quickly on the entire file in a streaming fashion, keeping the memory footprint small.</p><h3>Acero streaming engine</h3><p>What is Acero, you might wonder. <a href="https://arrow.apache.org/docs/python/api/acero.html">Acero</a> is an experimental streaming engine for running queries on large data. In Acero, you specify the processing pipeline declaratively, using several building blocks like projections, filters, and aggregations. You can choose from a wide range of predefined compute functions and crucially, you can also define your own custom functions (User Defined Functions, UDFs for short). The UDFs are fairly easy to write: you worry only about the transformations you want to perform. Acero figures out the rest. What’s more, you can use several languages to do so, we use Python for the data type detection pipeline and Cython for the pipeline we use to read the CSV data using the detected types. If SQL is more up your alley, you can use <a href="https://substrait.io">Substrait</a> to <a href="https://arrow.apache.org/docs/python/api/substrait.html#api-substrait">generate</a> the Acero query plan from an SQL query.</p><h3>The type detection pipeline</h3><p>From a high-level perspective, our type detection pipeline is very simple: it has one source node reading the CSV file and one projection node running the UDF detection algorithm. Ideally, there would also be an aggregation node at the end that would aggregate the results of each projection batch. Unfortunately, Acero does not seem to support UDFs in the aggregation nodes yet, so we run the aggregation in pure Python.</p><p>The detection UDF is run in parallel for every column in isolation and works like this. For each batch of values in a column:</p><h4>We detect which values are null or empty.</h4><p>We use <a href="https://arrow.apache.org/docs/python/generated/pyarrow.compute.match_substring_regex.html">regular expressions</a> to detect booleans, integers, and doubles.</p><pre>import pyarrow.compute as pc<br><br>is_boolean_vec = pc.match_substring_regex(<br>    array,<br>    # values taken from the defaults in pyarrow.csv.convert_options<br>    pattern=r&quot;^$|^(true|false|0|1)$&quot;,<br>    ignore_case=True,<br>    memory_pool=ctx.memory_pool,<br>)</pre><h4>We use regular expressions and the strptime function</h4><p>To detect possible date formats (based on a set of supported date formats).</p><h4>We return the following values</h4><ul><li>All the types the values in the batch conform to <strong>order by the specificity</strong> (e.g. integer is more specific than a double).</li><li>All the <strong>date formats</strong> that can be used to parse all non-empty values in the batch as a valid date.</li><li>Whether any of the values in the batch is null or empty.</li><li>Whether all of the values in the batch are null or empty.</li></ul><p>We then aggregate the results for all the batches for each column so that we get the final result:</p><ul><li>The most specific type usable for the column.</li><li>All the date formats that can be used to parse all the non-empty values in all the batches.</li><li>A flag indicating whether the column is nullable: i.e., it contains at least one value that is null or empty.</li></ul><h3>Reading a preview</h3><p>To allow the user to make an informed decision whether we “understood” the file properly and to allow them to pick the correct date format from those that we detected as suitable, we read a small sample of the data using the options we intend to use once the file is confirmed by the user. We return this preview as a part of the response, along with all the options and configurations we detected.</p><p>You might wonder, “Why does the user need to pick a date format?”. This is to handle situations where the date values are ambiguous. Imagine a file that only has these two values in a column: <strong>01/01/2024</strong> and<strong> 01/02/2024</strong>. Do these correspond to January 1st and 2nd? Or are they January 1st and February 1st? Only the user knows which is the case, so in these (admittedly rare) cases, they need to pick the correct date format for us to use.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0fQO-F_SvHIekBIO" /></figure><h3>Using the CSV file as a source of data</h3><p>Once the user confirms the CSV file is correctly parsed, the file is moved to the production area of the file storage, and a manifest file with all the metadata is created. When there is a computation run that needs to access the CSV data, it uses the metadata in the manifest to prepare a <a href="https://arrow.apache.org/docs/python/generated/pyarrow.RecordBatchReader.html">RecordBatchReader</a> that uses another Acero pipeline with another UDF for reading the date columns using the correct date format. The UDF is a thin wrapper around the <a href="https://arrow.apache.org/docs/python/generated/pyarrow.compute.strftime.html">strftime</a> function written in Cython that does not fail on empty values but fails on invalid non-empty values. The default strftime either fails on empty values or returns null for anything it cannot parse, neither of which is what we want.</p><p>The resulting RecordBatchReader can then be consumed by the rest of Longbow, business as usual. There is a dedicated article coming about that particular part of Longbow, so stay tuned!</p><h3>Summary</h3><p>CSV files are one of the most used formats for storing structured data. Their relative looseness and simplicity make them easy to produce, but they are also quite challenging to read and parse automatically. We have outlined the way we do it for Longbow, leveraging the DuckDB CSV sniffing functionality and the Apache Arrow capabilities: its CSV module and the Acero streaming engine.</p><h3>Want to learn more?</h3><p>As always, I’m eager to hear what you think about the direction we are taking with our Platform. Feel free to reach out to us on the <a href="https://www.gooddata.com/slack/?utm_medium=blogpost&amp;utm_source=medium.com&amp;utm_campaign=csv_in_analytics_04_2024&amp;utm_content=autor_dan">GoodData community Slack</a>.</p><p>Want to try it out for yourself? Consider using our <a href="https://www.gooddata.com/trial/?utm_medium=blogpost&amp;utm_source=medium.com&amp;utm_campaign=csv_in_analytics_04_2024&amp;utm_content=autor_dan">free trial</a>. Want to learn what else we are cooking at GoodData? Join <a href="https://www.gooddata.com/platform/labs/?utm_medium=blogpost&amp;utm_source=medium.com&amp;utm_campaign=csv_in_analytics_04_2024&amp;utm_content=autor_dan">GoodData Labs</a>!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=34ee7fa74754" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gooddata-developers/csv-files-in-analytics-taming-the-variability-34ee7fa74754">CSV Files in Analytics: Taming the Variability</a> was originally published in <a href="https://medium.com/gooddata-developers">GoodData Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flexible caching and storage with Flight RPC]]></title>
            <link>https://medium.com/gooddata-developers/flexible-caching-and-storage-with-flight-rpc-725ec57c37e2?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/725ec57c37e2</guid>
            <category><![CDATA[apache-arrow-flight]]></category>
            <category><![CDATA[cache]]></category>
            <category><![CDATA[apache-arrow]]></category>
            <category><![CDATA[analytics]]></category>
            <category><![CDATA[gooddata]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Mon, 25 Mar 2024 11:05:02 GMT</pubDate>
            <atom:updated>2024-03-28T08:06:46.514Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*chf6lQgG24BAUXRria-Omg.png" /><figcaption>This is part of a series about <a href="https://www.gooddata.com/platform/flexquery/?utm_medium=blogpost&amp;utm_source=medium.com&amp;utm_campaign=flexible_caching_and_storage_flexquery_03_2027&amp;utm_content=autor_dan">FlexQuery</a> and the <a href="https://medium.com/gooddata-developers/project-longbow-35f5a860bb9b">Longbow engine</a> powering it.</figcaption></figure><p>Storing any kind of large data is tricky. So when we designed our Longbow engine, for our Analytics Stack, we did so with flexible caching and storage in mind.</p><p>Longbow is our framework based on <a href="https://arrow.apache.org/">Apache Arrow</a> and <a href="https://arrow.apache.org/docs/format/Flight.html">Arrow Flight RPC</a>, for creating modular, scalable, and extensible data services. It is essentially the heart of our Analytics Stack ( FlexQuery). For a more bird’s eye view of Longbow and to understand its place within FlexQuery, see the <a href="https://medium.com/gooddata-developers/project-longbow-35f5a860bb9b">architectural</a> and <a href="https://medium.com/gooddata-developers/building-a-modern-data-service-layer-with-apache-arrow-33ace768e3f1">introductory</a> articles in this series.</p><p>Now let’s have a look at the tiered storage service for the Flight RPC in Longbow, its trade-offs and our strategies to tackle them.</p><p>If you want to learn more about the Flight RPC I highly recommend the <a href="https://medium.com/gooddata-developers/arrow-flight-rpc-101-583517ad1539">Arrow Flight RPC 101</a> article written by Lubomir Slivka.</p><h3>The storage trade-offs</h3><p>To set the stage for the Longbow-specific part of the article, let us briefly describe the problems we tackled and some basic motivations. When storing data, we need to be mindful of several key aspects:</p><ul><li><strong>Capacity</strong></li><li><strong>Performance</strong></li><li><strong>Durability</strong></li><li><strong>Cost</strong></li></ul><p>Setting them up and balancing them in cases when they go against one another is a matter of carefully defining the requirements of the particular application and can be quite situation-specific.</p><h3>Cost v. Performance</h3><p>The first balancing decision is how much we are willing to spend on increased performance. For example, when storing a piece of data being accessed by many users in some highly visible place in our product, we might favor better performance for a higher cost because the end-user experience is worth it. Conversely, when storing a piece of data that has not been accessed for a while, we might be ok with it being on a slower (and cheaper) storage even at the potential cost of it taking longer to be ready when accessed again. We can also be a bit smarter here and make it so that only the first access will be slower (more on that later).</p><h3>Capacity v. Performance</h3><p>Another balancing decision is about where we draw the line between data sizes that are too big. For example, we might want to put as many pieces of data into memory as possible: memory will likely be the best-performing storage available. At the same time though, memory tends to be limited and expensive, especially if you work with large data sets or very many of them: you cannot fit everything into memory and “clog” the memory with one huge flight instead of using it for thousands of smaller ones would be unwise.</p><h3>Durability</h3><p>We also need to decide on how durable the storage needs to be. Some data changes rarely but is very expensive to compute, it might even be impossible to compute again (e.g. a user-provided CSV file): such data might be a good candidate for durable storage. Other types of data can be changing very often and/or be relatively cheap to compute: non-durable storage might be better there. Also, there can be other non-technical circumstances: compliance or legal requirements might force you to avoid any kind of durable storage altogether.</p><h3>Isolation and multi-tenancy</h3><p>Yet another aspect to consider is whether we want to isolate some data from the rest or handle it differently, especially in a multi-tenant environment. You might want to give a particular tenant a higher capacity of storage space because they are on some advanced tier of your product. Or you might want to make sure some of the data is automatically removed after a longer period of time for some tenants. The storage solution should give you mechanisms to address these requirements. It is also a monetization leverage — yes, even we have to eat something ;-)</p><h3>Storage in Longbow</h3><p>When designing and developing Longbow, we aimed to make it as universally usable as possible. As we have shown in the previous section, flexibility in the storage configuration is paramount to making a Longbow deployment efficient and cost-effective.</p><p>Since Longbow builds on the Flight RPC protocol (we go into much more detail in the <a href="https://medium.com/gooddata-developers/project-longbow-35f5a860bb9b">Project Longbow article</a>), it stores the individual pieces of data (a.k.a flights) under flight paths. Flight paths are unique identifiers of the related flights. They can be structured to convey some semantic data by having several segments separated by a separator (in our case it is a slash).</p><p><em>In this context, you can think of a flight as a file on a filesystem and a flight path as a file path to it: like it, it is a slash-separated string like </em><em>cache/raw/postgres/report123.</em></p><p>The flight paths can be used by the Flight RPC commands to reference the particular flights. Flight RPC, however, does not impose any constraints on how exactly the actual flight data should be stored: as long as the data is made available when requested by a Flight RPC command, it does not care where it comes from. We take advantage of this fact and we make use of several types of storage to provide the data to Flight RPC.</p><p>The storage types in Longbow are divided into these categories:</p><ul><li><strong>Shard </strong>— local, ephemeral storage<br> — Memory<br> — Memory-mapped disk<br> — Disk</li><li><strong>External</strong>, persistent storage</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JCxsDa1rnngRZQoQnpnvrQ.png" /><figcaption><em>Visualization of the relationship between Longbow shards and different tiers of storage. The shard lingo is taken from database architecture.</em></figcaption></figure><p>As you can see, the lower in the storage hierarchy we go, the slower, larger, and cheaper the storage gets, and vice versa. Also, only the external durable storage survives Longbow shard restarts. To expose these layers and to make them configurable, we built an abstraction on top of them that we call Storage classes.</p><h3>Storage classes</h3><p>A storage class encapsulates all the storage-related configurations related to a subset of flights. It has several important properties:</p><ul><li>It can be applied only to some flights: you can have different storage classes for different types of flights.</li><li>The settings in the storage class can be tiered: you can mix and match different types of storage and flights can be moved between them.</li></ul><p>Longbow uses the storage class definitions to decide where to physically store an incoming flight and how to manage it.</p><h3>Storage class settings</h3><p>Storage classes have different settings that define the storage class as a whole, another set of settings that define different so-called cache tiers, and another set that can govern the limits of several storage classes. Let’s focus on the storage-class-wide ones first.</p><h4>Flight path prefix</h4><p>Each storage class is applied to a subset of the flight paths. More specifically, each storage class defines a <strong>flight path prefix</strong>: only flights that share that flight path prefix are affected by that particular storage class. This allows us to handle different types of data differently by constructing their flight paths systematically thus handling the tradeoffs described earlier differently for data with different business meanings.</p><p>The storage classes can have flight path prefixes that are substrings of each other: in case of multiple prefixes matching the given flight path, the storage class with the longest matching prefix is used.</p><p><em>Going back to the filesystem path analogy, you can think of the path prefix as a path to the folder you want to address with the particular settings. Folders deeper in the tree can have their own configuration, overriding that of the parent folders.</em></p><h4>Durability</h4><p>Storage classes must specify the level of storage <strong>durability</strong> they guarantee. Currently, we support three levels of durability:</p><ul><li><em>none</em> — the storage class does not store the flights in any durable storage. Data will be lost if the Longbow node handling that particular flight runs out of resources and needs to evict some flights to make room for new ones, crashes, or is restarted</li><li><em>weak</em> — the storage class will acknowledge the store operation immediately (before storing the data in durable storage). Data can be lost if the Longbow node crashes <em>during</em> the upload to the durable storage. However, the happy-path scenarios can have better performance.</li><li><em>strong</em> — the storage class will only acknowledge the store operation after the data has been fully written to the durable storage. Data will never be lost for acknowledged stores, but the performance may suffer waiting for the durable write to complete.</li></ul><p>These levels allow us to tune the performance and behavior of different data types. For data we cannot afford to lose (for example direct user uploads), we would choose strong durability, for data that can be easily recalculated in the rare case of a shard crash, weak might provide a better performance overall.</p><p>Storage classes with any durability specified will store the incoming data in their ephemeral storage first (if it can fit) and then create a copy in the durable storage. Whenever some flight is evicted from the ephemeral storage, it can still be restored there from the durable storage if it is requested again.</p><h4>Durable Storage ID and Prefix</h4><p><strong>Durable storage ID and prefix</strong> tell the storage class which durable storage to use and whether or not to put the data there under some root path (so that you can use the same durable storage for multiple storage classes and keep the data organized). This also means you can use different durable storage and in the future, this would also enable some “bring your own storage” types of use cases for your users.</p><h4>Flight Time-To-Live (TTL)</h4><p>With <strong>flight Time-To-Live (TTL)</strong>, storage classes can specify how long “their” flights will remain in the system. After that time passes, flights will be made unavailable.</p><p>In non-durable storage classes, the flights might become unavailable sooner: if resources are running out, the storage class can evict the flights to make space for newer ones.</p><p>In durable storage classes, the TTL is really the time they will be available for. In case the flight is evicted from the ephemeral storage tier (more on that later), it will be restored from the durable storage on next access. After the TTL passes, the flight will also be deleted from the durable storage.</p><h4>Flight replicas</h4><p>Optionally, the <strong>flight replicas</strong> setting directs the storage class to create multiple copies of the flights across different Longbow nodes. This is not meant to be a resilience mechanism, rather, it can be used to improve performance by making the flights available on multiple nodes.</p><h3>Cache tier settings</h3><p>As described earlier, each Longbow shard has several layers of ephemeral storage resources and if configured, also external durable storage (e.g. AWS S3 or network-attached storage (NAS)). To utilize each of these layers as efficiently as possible, storage class settings can configure the usage of each of these tiers separately. There are also policies that automatically move data to a slower tier after that piece of data was not accessed for some time and vice versa, moving the data to a faster tier after it has been accessed repeatedly over a short period of time.</p><p>The cache tiers each have several configuration options to allow for all of these behaviors.</p><h4>Storage type</h4><p>Tiers must specify the <strong>type of storage</strong> they manage. There are several options:</p><ul><li><em>memory</em> — the data is stored in the memory of the Longbow shard.</li><li><em>disk_mapped</em> — the data is stored on the disk available to the Longbow shard and memory mapping is used when accessing the data.</li><li><em>disk</em> — the data is stored on the disk available to the Longbow shard. This disk is wiped whenever the shard restarts.</li></ul><p>The storage types are shared by all the tiers of all the storage classes in effect (there is one memory after all), so once the storage is running out of space, flights from across the storage classes will be evicted.</p><p>For storage classes with no durability, eviction means deleting the flight forever.</p><p>For storage classes with durability, eviction deletes the flight copy from the ephemeral storage but keeps the copy in durable storage. This means that if the evicted flight is requested, it can be restored from the durable copy back into the more performant ephemeral storage.</p><h4>Max flight size and Upload Spill</h4><p>Each tier can specify the <strong>maximum size of flights</strong> it can accept. This is to prevent situations when one large flight being uploaded would lead to hundreds or thousands of smaller flights being evicted: it is usually favorable to be able to serve a lot of users efficiently with smaller flights than only a handful with a few large ones. There is also a setting allowing the flights larger than the limit to either be rejected straight away or to spill over to another storage tier (e.g. from memory to the disk instead).</p><p>Since the flight data is streamed during the upload, the final size is not always known ahead of time. So the upload starts writing the data to the first tier and only when it exceeds the limit, the already uploaded data is moved to the next tier, and the rest of the upload stream is also redirected there. For cases when the final size is known ahead of time, the client can provide it at the upload start using a specific Flight RPC header, to avoid the potentially wasteful spill process.</p><h4>Priority and Spill</h4><p>When a storage type is running out of resources, it might need to start <strong>evicting</strong> some of the flights to make space for newer ones. By default, the eviction is driven using the least-recently-used (LRU) policy, but for situations where more granular control is needed, the storage tiers can specify a priority: flights from a tier with a higher priority will be evicted only after all the flights with lower priority are gone.</p><p>Related to this, there is also a setting that can cause the data to be moved to another storage type instead of being evicted from the ephemeral storage altogether (similarly to the <strong>Upload Spill</strong>).</p><h4>Move after and Promote after</h4><p>Tiers can also specify a time period after which the flights are moved to another (lower) tier. This is to prevent the “last-minute” evictions that would happen when the given storage tier is running out of resources.</p><p>Somewhat inverse to the Move after mechanism, tiers can proactively promote a flight to a higher tier after it was accessed a defined number of times over a defined period of time. This handles the situation where a lot of users start accessing the same “stale” flight at the same time: we expect that even more are coming so to improve the access time for them, we move the flight to a faster tier.</p><p>Tuning these two settings allows us to strike a good balance between having the flights most likely to be accessed the most in the fastest cache tier while being able to ingest new flights as well.</p><h3>Limit policies</h3><p>To have even more control, the configuration also offers something we call <strong>Limit policies</strong>. These can affect how many resources in total a particular storage class takes up in the system. By default, without any limit policies, all the storage classes share all the resources available. Limit policies allow you to restrain some storage classes to only store data up to a limit. You can limit both the amount of data in the non-durable storage only and the total amount including the durable storage.</p><p><em>For example, you can configure the policies in a way that limits cache for report computations to 1GB of data in the non-durable storage but leaves it unlimited capacity in the durable storage. Or you can also impose a limit on the durable storage (likely to limit costs).</em></p><p>There are several types of limit policies catering to different types of use cases. One limit policy can be applied to several storage classes and more than one limit policy can govern a particular storage class.</p><h4>Standard limit policies</h4><p>These are the most basic policies, they set a limit on a storage class as a whole i.e. on all the flight paths sharing the storage class’ flight path prefix. They are useful for setting a “hard” limit on storage classes that you fine-tune using other limit policy types.</p><h4>Segmented limit policies</h4><p>Segmented limit policies allow you to set limits on individual flight path subtrees governed by a storage class. <em>For example, if you have flight paths like cache/postgres and cache/snowflake and a storage class that covers the cache path prefix, you can set up such a policy that limits data from each database to take at most 1GB per database type and it would automatically cover even database types added in the future.</em></p><h4>Hierarchical limit policies</h4><p>Offering even more control than with the segmented limit policies, hierarchical limit policies allow you to model more complex use cases limiting on more than one level of the flight paths.</p><p><em>For example, you have a storage class that manages flights with the cache prefix. On the application level, you make sure that the flight paths for this storage class always include some kind of tenant id as the second part of the flight path, the third part is the identifier of a data source the tenant uses and finally, the fourth segment is the cache itself: cache/tenant1/dataSource1/cacheId1. Hierarchical limit policies allow you to do things like limiting tenant1 to 10GB in total and also allow them to put 6GB towards dataSource1 because they know it produces bigger data while keeping all the other tenants at 5GB by default.</em></p><h3>Summary</h3><p>As we have shown, storing any kind of large data certainly isn’t straightforward. It has many facets that need to be carefully considered. Any kind of data storage system needs to be flexible enough to allow the users to tune it according to their needs.</p><p>Longbow comes equipped with a meticulously designed tiered storage system exposed via Flight RPC that enables the users to set it up to cater to whatever use case they might have. It takes advantage of several different storage types and plays to their strengths whether it is size, speed, durability, or cost.</p><p>In effect, Longbow’s cache system provides virtually unlimited storage size thanks to the very cheap durable storage solutions while keeping the performance much better for the subset of data that is being actively used.</p><h3>Want to learn more?</h3><p>As we mentioned in the introduction, this is part of a series of articles, where we take you on a journey of how we built our new analytics stack on top of Apache Arrow and what we learned about it in the process.</p><p>Other parts of the series are about the <a href="https://medium.com/gooddata-developers/building-a-modern-data-service-layer-with-apache-arrow-33ace768e3f1">Building of the Modern Data Service Layer</a>, <a href="https://medium.com/gooddata-developers/project-longbow-35f5a860bb9b">Project Longbow</a>, details about the <a href="https://medium.com/gooddata-developers/arrow-flight-rpc-101-583517ad1539">Flight RPC</a>, and last but not least, how good the <a href="https://medium.com/gooddata-developers/duckdb-meets-apache-arrow-169e917a2d8d">DuckDB quacks with Apache Arrow</a>!</p><p>As you can see in the article, we are opening our platform to an external audience. We not only use (and contribute to) state-of-the-art open-source projects, but we also want to allow external developers to deploy their services into our platform. Ultimately we are thinking about open-sourcing the Longbow. Would you be interested in it? Let us know, your opinion matters!</p><p>If you’d like to discuss our analytics stack (or anything else), feel free to join our <a href="https://www.gooddata.com/slack/?utm_medium=blogpost&amp;utm_source=medium.com&amp;utm_campaign=flexible_caching_and_storage_flexquery_03_2027&amp;utm_content=autor_dan">Slack community</a>!</p><p>Want to see how well it all works in practice, you can try the <a href="https://www.gooddata.com/trial/?utm_medium=blogpost&amp;utm_source=medium.com&amp;utm_campaign=flexible_caching_and_storage_flexquery_03_2027&amp;utm_content=autor_dan">GoodData free trial</a>! Or if you’d like to try our new experimental features enabled by this new approach (AI, Machine Learning and much more), feel free to sign up for our <a href="https://www.gooddata.com/platform/labs/?utm_medium=blogpost&amp;utm_source=medium.com&amp;utm_campaign=flexible_caching_and_storage_flexquery_03_2027&amp;utm_content=autor_dan">Labs Environment</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=725ec57c37e2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gooddata-developers/flexible-caching-and-storage-with-flight-rpc-725ec57c37e2">Flexible caching and storage with Flight RPC</a> was originally published in <a href="https://medium.com/gooddata-developers">GoodData Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Polyglot Apache Arrow: Java and Python Perspective]]></title>
            <link>https://medium.com/gooddata-developers/polyglot-apache-arrow-java-and-python-perspective-bf2ce020e27d?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/bf2ce020e27d</guid>
            <category><![CDATA[java]]></category>
            <category><![CDATA[gooddata]]></category>
            <category><![CDATA[apache-arrow]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Mon, 31 Jul 2023 10:37:45 GMT</pubDate>
            <atom:updated>2023-07-31T10:37:45.676Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IiLe1-JfTrw9o3M32Oeu8g.png" /></figure><p>Apache Arrow, a versatile, analytics-focused in-memory data format, offers the flexibility to work with data seamlessly across multiple programming languages, such as Java and Python. While utilizing Apache Arrow in more than one language may initially seem straightforward — write a feature in one language and ‘translate’ it to another — there are certain nuances and challenges that may arise. In this article, we aim to shed light on some of these challenges we encountered at <a href="https://www.gooddata.com/?utm_medium=blogpost&amp;utm_source=medium.com&amp;utm_campaign=gooddata_apache_arrow_072023&amp;utm_content=autor_dan">GoodData</a>. We do this not to criticize the Arrow project, but rather to provide insights and solutions for fellow developers who may encounter similar obstacles.</p><h3>Issues encountered</h3><p>To keep things organized, let’s split the tricky parts into three categories:</p><ul><li>Missing APIs</li><li>Different API philosophies</li><li>API inconsistencies</li></ul><h3>Missing cancel in FlightStreamWriter in Python</h3><p>The first difference we encountered has to do with how the client can cancel a put operation.</p><p>There are several reasons the client might want to stop the writing half-way. For example it reads from some underlying datasource batch-by-batch and sends each batch to the Flight server. It can then happen that the underlying datasource raises an exception. The flight client then wants to signal to the server: “I am canceling this put operation, disregard whatever I have sent you so far”.</p><p>In Java, the process is simple: clients call the <a href="https://arrow.apache.org/docs/java/reference/index.html">error</a> method of the ClientStreamListener returned by the <a href="https://arrow.apache.org/docs/java/reference/index.html">startPut</a> method, which will close the stream and signal the cancellation to the server (where <em>server_call_context.is_cancelled()</em> returns True). Consequently, the server can react accordingly.</p><p>In Python, though, the client cannot cancel the flight put operation in a way the server would be able to detect. It can raise an exception, but the server has no way of knowing which of these cases occurred:</p><ol><li>The client is done writing and just did not call done_writing and the data sent by the client should be persisted by the server.</li><li>The client wants to cancel the operation, and any data sent by the client should not be persisted by the server.</li></ol><p>Currently, there is no way of doing this in Python (we’ve opened an <a href="https://github.com/apache/arrow/issues/34431">issue</a> for it). Fortunately for us, we currently do not use the Python client in this context. However, should we start, or decide to make the client public, we would have to handle this somehow.</p><h3>Different API philosophies</h3><h4>Start_put: tuple in Python, listener as an argument in Java</h4><p>Since we are running the Arrow Flight RPC servers in a High-Availability (HA) environment, the logic behind putting a flight there is more involved. First, we need to contact the cluster with the flightPath we would like to put and it responds with a list of locations of the nodes that can accept that particular path. Once we have the list, we try each node in the list and use the first node that successfully accepts the put to perform the operation.</p><p>To avoid duplicating this logic across the applications, we encapsulated it in our clients. First, we implemented this logic in Python.</p><pre>def _start_put(<br>    self,<br>    flight_path: str,<br>    schema: pyarrow.Schema,<br>    options: pyarrow.flight.FlightCallOptions,<br>) -&gt; tuple[pyarrow.flight.FlightStreamWriter, pyarrow.flight.FlightMetadataReader]:<br>    # this method handles the initiation of the do_put operation while also handling situations when the cluster<br>    # changes between the operations (the client that was ok before is now broken): in that case we need to try<br>    # again with another client. the tricky part is that we only know whether the client is ok or not when we<br>    # initiate the do_put operation: only then can we decide to query the shards again<br>    descriptor = pyarrow.flight.FlightDescriptor.for_path(flight_path)<br><br>    writer: Optional[pyarrow.flight.FlightStreamWriter] = None<br>    metadata_reader: Optional[pyarrow.flight.FlightMetadataReader] = None<br>    error: Optional[pyarrow.flight.FlightError] = None<br>    put_started = time.time()<br><br>    while (<br>        writer is None<br>        and time.time() - put_started &lt; self._put_establishing_timeout<br>    ):<br>        try:<br>            error_count = 0<br>            shard_count = 0<br>            for shard in self.shards_for_put(flight_path=flight_path):<br>                try:<br>                    shard_count += 1<br>                    writer, metadata_reader = shard.do_put(<br>                        descriptor=descriptor, schema=schema, options=options<br>                    )<br>                    break<br>                except pyarrow.flight.FlightUnavailableError as e:<br>                    error_count += 1<br>                    error = e<br>            # if all shards failed raise an error so that the placement is tried again after backoff, not immediately<br>            if shard_count &gt; 0 and shard_count == error_count:<br>                raise cast(pyarrow.flight.FlightError, error)<br>        except pyarrow.flight.FlightUnavailableError as e:<br>            error = e<br>            time.sleep(0.25)<br><br>    if writer is None or metadata_reader is None:<br>        # if the writer is not assigned, we must have hit an error<br>        assert error is not None<br>        raise error<br><br>    return writer, metadata_reader</pre><p>This is pretty straightforward, so when we wanted to replicate a similar logic in Java, we approached it with a “just rewrite this to Java” attitude. As it turns out, this it not so simple, as there are two substantial differences between Java and Python in this area:</p><ol><li>The semantics differ for the writer.</li><li>The API shape is quite different in Java.</li></ol><p>The first difference is between what state the writer is returned in. In Python, when the writer is returned, you can start writing to it right away. In Java, however, you should first check the isReady flag (or even better subscribe to <a href="https://arrow.apache.org/docs/java/reference/index.html">setOnReadyHandler</a>) before writing to the stream. This complicates our node probing logic a bit.</p><pre>public StartPutResult startPut(FlightDescriptor descriptor,<br>                               VectorSchemaRoot root,<br>                               CallOption... options) throws FlightRuntimeException, InterruptedException {<br>    ClientStreamListener writer = null;<br>    CustomPutListener metadataListener = null;<br>    var startTime = System.currentTimeMillis();<br>    FlightRuntimeException error = null;<br>    var resolveLocationsCalled = false;<br>    while (writer == null &amp;&amp; System.currentTimeMillis() - startTime &lt; putEstablishingTimeout) {<br>        // first, try getting all the locations that can accept the flight<br>        List&lt;Location&gt; locations;<br>        try {<br>            var distributionNodes = getDistribution(descriptor, options);<br>            locations = nodesToLocations(distributionNodes);<br>        } catch (FlightRuntimeException e) {<br>            if (isRecoverable(e)) {<br>                if (isConnectionFailure(e) &amp;&amp; !resolveLocationsCalled) {<br>                    locationProvider.resolveLocations();<br>                    resolveLocationsCalled = true;<br>                }<br>                error = e;<br>                Thread.sleep(putEstablishingGracePeriod); // back off for a bit before retrying<br>                continue;<br>            } else {<br>                throw e;<br>            }<br>        }<br><br>        // then try each location in turn to find the first one that manages to establish the put operation<br>        for (var location : locations) {<br>            try {<br>                metadataListener = new CustomPutListener();<br>                writer = flightClientsAdapter.startPut(location, descriptor, root, metadataListener, options);<br>                Thread.sleep(putEstablishingGracePeriod); // give it some time to get ready, without it, the isReady would never be true<br>                if (writer.isReady()) {<br>   // we have the writer that is ready to accept flights: break out of the cycle<br>                    break;<br>                } else {<br>                    // the writer was not ready in time: clean up and try another<br>                    var toCleanUp = writer;<br>                    writer = null;<br>                    metadataListener = null;<br>                    error = null;<br>                    toCleanUp.error(new Exception(&quot;client not ready in time, cleaning up&quot;));<br>                    toCleanUp.getResult();<br>                }<br>            } catch (FlightRuntimeException e) {<br>                // we assume that FlightStatusCode.CANCELLED encountered here always means we hit<br>                // the &quot;client not ready in time&quot; and should therefore try again<br>                var isClientNotReadyInTimeError = e.status().code() == FlightStatusCode.CANCELLED;<br>                var shouldRetry = isClientNotReadyInTimeError || isRecoverable(e);<br>                if (shouldRetry) {<br>                    if (isConnectionFailure(e) &amp;&amp; !resolveLocationsCalled) {<br>                        locationProvider.resolveLocations();<br>                        resolveLocationsCalled = true;<br>                        break;<br>                    }<br>                    error = e;<br>                } else {<br>                    throw e;<br>                }<br>            }<br>        }<br>    }<br><br>    if (writer == null) {<br>        if (error == null) {<br>            throw new ArrowClientNoNodeForPutException();<br>        } else {<br>            throw error;<br>        }<br>    }<br><br>    return new StartPutResult(writer, metadataListener);<br>}</pre><p>As you can see, the logic is more elaborate than its Python counterpart (even though the ideas behind them are the same).</p><p>The second difference is that in Python you get both <a href="https://arrow.apache.org/docs/python/generated/pyarrow.flight.FlightClient.html#pyarrow.flight.FlightClient.do_put">the writer and the metadata_reader</a> for both directions of the bidi channel. In Java, however, you only get the writer and you need to provide your own listener implementation (defined by the <a href="https://github.com/apache/arrow/blob/c6dca7d3c9611013a4affbceccb0699d677447d0/java/flight/flight-core/src/main/java/org/apache/arrow/flight/FlightClient.java#L510">PutListener interface</a>). Due to the retry logic we have in place, we need to actually return this listener (and control its creation: we might need more than one and return the one associated with the node that is used in the end).</p><p>To make providing your own PutListener easier, there are two implementations provided by the Arrow Java client: <a href="https://github.com/apache/arrow/blob/c6dca7d3c9611013a4affbceccb0699d677447d0/java/flight/flight-core/src/main/java/org/apache/arrow/flight/SyncPutListener.java">SyncPutListener</a> and <a href="https://github.com/apache/arrow/blob/c6dca7d3c9611013a4affbceccb0699d677447d0/java/flight/flight-core/src/main/java/org/apache/arrow/flight/AsyncPutListener.java">AsyncPutListener</a> you can use.</p><p>Since we want to read from the server only after the whole writing process is complete, SyncPutListener seemed like the more fitting choice. We wanted to extend it with some domain-specific methods to make it easier to use in our context, but alas, the SyncPutListener is final and cannot be extended. We could make a new class and compose the SyncPutListener into it, but that seemed like quite a lot of boilerplate. Instead, we chose to extend the extensible AsyncPutListener. This class allows you to override only one method and build your logic around it.</p><pre>package com.gooddata.demo;<br><br>import org.apache.arrow.flight.AsyncPutListener;<br>import org.apache.arrow.flight.PutResult;<br><br>import java.nio.charset.StandardCharsets;<br><br>public final class CustomPutListener extends AsyncPutListener {<br>    private boolean hasWaitedForResult = false;<br>    private String syncToken = null;<br><br>    /**<br>     * Returns the syncToken that can be used to enforce read-after-write consistency.<br>     * Must be called after {@link CustomPutListener#waitForComplete}.<br>     */<br>    public String getSyncToken() {<br>        if (!hasWaitedForResult) {<br>            throw new IllegalStateException(&quot;You must call waitForComplete before trying to access the sync token.&quot;);<br>        }<br>        if (syncToken == null || syncToken.isBlank()) {<br>            throw new IllegalStateException(&quot;Server did not send any sync token back.&quot;);<br>        }<br>        return syncToken;<br>    }<br><br>    @Override<br>    public void onNext(PutResult val) {<br>        var metadata = val.getApplicationMetadata();<br>        var charBuffer = StandardCharsets.UTF_8.decode(metadata.nioBuffer());<br>        this.syncToken = charBuffer.toString();<br>    }<br><br>    /**<br>     * Call this method to ensure the writing of the server-sent metadata is finished.<br>     * This must be done before accessing any of the properties provided by this class.<br>     */<br>    public void waitForComplete() {<br>        getResult();<br>        hasWaitedForResult = true;<br>    }<br>}</pre><h4>Mandatory done_writing in Python</h4><p>An especially tricky aspect of the Python client code is the requirement to call the <a href="https://arrow.apache.org/docs/python/generated/pyarrow.flight.FlightStreamWriter.html#pyarrow.flight.FlightStreamWriter.done_writing">done_writing</a> method once writing to the stream is complete. Otherwise, the server can get stuck waiting for the next batch that might never come. Below is a simple server code illustrating this:</p><pre>import pyarrow as pa<br>import pyarrow.flight as flight<br><br><br>class Server(flight.FlightServerBase):<br>    def __init__(self, location):<br>        super(Server, self).__init__(location=location)<br><br>    def do_put(self, context, descriptor, reader, writer):<br>        for batch in reader:<br>            print(f&quot;got batch {str(batch.data)}&quot;)<br>        # or even just reader.read_all()<br><br>        print(&quot;sending metadata back&quot;)<br>        writer.write(pa.py_buffer((42).to_bytes(8, byteorder=&quot;big&quot;)))<br><br><br>def main():<br>    server = Server(&quot;grpc://localhost:16661&quot;)<br>    server.serve()<br><br><br>if __name__ == &quot;__main__&quot;:<br>    main()</pre><p>This server works correctly with well-behaved clients. When faced with a not-so-well-behaved one like the one in the following code listing, it will get stuck:</p><pre>import pandas as pd<br>import pyarrow as pa<br>import pyarrow.flight as flight<br><br><br>def main():<br>    client = flight.connect(&quot;grpc://localhost:16661&quot;)<br>    df = pd.DataFrame(<br>        {<br>            &quot;n_legs&quot;: [None, 4, 5, None],<br>            &quot;animals&quot;: [&quot;Flamingo&quot;, &quot;Horse&quot;, None, &quot;Centipede&quot;],<br>        }<br>    )<br>    table = pa.Table.from_pandas(df)<br><br>    writer, reader = client.do_put(<br>        flight.FlightDescriptor.for_path(b&quot;fun/times&quot;), table.schema<br>    )<br><br>    writer.write_table(table)<br><br>    print(&quot;done putting&quot;)<br>    # if this line is uncommented, everything starts working fine<br>    # writer.done_writing()<br><br>    meta = reader.read()<br>    print(int.from_bytes(meta))<br><br><br>if __name__ == &quot;__main__&quot;:<br>    main()</pre><p>Unless the <em>writer.done_writing()</em> is called, both the client and the server get stuck: the server waiting for batches from the client, the client waiting for a message from the server.</p><p>The <a href="https://arrow.apache.org/docs/python/generated/pyarrow.flight.FlightStreamWriter.html#pyarrow.flight.FlightStreamWriter.done_writing">documentation</a> does not mention the significance of the <em>done_writing</em> call. It is apparently <a href="https://github.com/apache/arrow/issues/34415">a known issue</a>, one that will hopefully get resolved with the introduction of <a href="https://github.com/apache/arrow/issues/34221">async APIs</a>.</p><p>In Java, this is not an issue: once the client is closed, the stream is closed as well and everything works as expected (although you should also call the completed method, as <a href="https://arrow.apache.org/cookbook/java/flight.html#put-data">mentioned</a> in the Arrow Java Cookbook).</p><h3>API inconsistencies</h3><h4>The mismatch between metadata types: bytes in Python, String in Java</h4><p>The most recently discovered tricky part had to do with custom flight metadata. Quite some time ago, we added a custom metadata field to our flights. This field contains a serialized protobuf command that was used to generate this flight (an opt-in feature useful for debugging).</p><p>In Python, we serialized this protobuf object to bytes using the standard protobuf serialization (using the <a href="https://googleapis.dev/python/protobuf/latest/google/protobuf/message.html#google.protobuf.message.Message.SerializeToString">SerializeToString</a> method of the protobuf-generated object). We stored this serialized object in the flight metadata. This worked in Python no problem: writing the metadata was ok, reading it as well (we even had tests for both).</p><p>Some time later we tried to access this metadata field from Java and started getting exceptions:</p><blockquote>java.lang.IllegalArgumentException: Invalid UTF-8: Illegal leading byte in 2 bytes utf.</blockquote><p>After a bit of not-so-easy debugging (especially due to our error handling unintentionally swallowing the original exception, which we have since improved), we discovered that the problem is that in Java, each of the metadata values must be a String, not a bytes array (see the particular <a href="https://github.com/apache/arrow/blob/37975d52e7bf829f9e5a23a65d584523d7616a4d/java/vector/src/main/java/org/apache/arrow/vector/types/pojo/Schema.java#L100">line</a> that raises the exception).</p><p>To work around this limitation, we now store the command base64-encoded in the schema: this admittedly makes the schema larger, but is currently the only way around this issue.</p><h3>Conclusion</h3><p>In conclusion, although working with Apache Arrow in multiple languages such as Java and Python offers great flexibility, there are certain challenges and differences that developers need to be aware of. This article has highlighted some of these challenges encountered during the implementation process.</p><p>Despite these challenges, it is important to emphasize that the Apache Arrow project itself is highly appreciated and enjoyable to work with. By highlighting the encountered challenges, this article aims to provide awareness and guidance for developers who may face similar difficulties.</p><p>As the Apache Arrow community continues to evolve and improve the project, we expect that some of these challenges will be addressed, leading to smoother cross-language integration and enhanced usability.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bf2ce020e27d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/gooddata-developers/polyglot-apache-arrow-java-and-python-perspective-bf2ce020e27d">Polyglot Apache Arrow: Java and Python Perspective</a> was originally published in <a href="https://medium.com/gooddata-developers">GoodData Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using QR codes to facilitate testing config in React Native]]></title>
            <link>https://medium.com/vzp-engineering/using-qr-codes-to-facilitate-testing-config-in-react-native-7e87233a1af7?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/7e87233a1af7</guid>
            <category><![CDATA[user-experience]]></category>
            <category><![CDATA[react-native]]></category>
            <category><![CDATA[qr-code]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Fri, 24 Aug 2018 11:59:53 GMT</pubDate>
            <atom:updated>2018-08-24T11:59:53.846Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wabzVul8gPtcI5JZ88N3Kg.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/photos/gpKe3hmIawg">Rima Kruciene</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure><p>While creating a proof of concept for a mobile app for one of our products in <a href="https://facebook.github.io/react-native/">React Native</a>, we came across an interesting problem to solve — how to allow our testers to switch the URLs of the API endpoints for the application to get data from. This is necessary because we need to test the app with different versions of our APIs, for several reasons:</p><ul><li>When our API changes, we need to test the mobile app against it before releasing it.</li><li>We can manipulate the data on the testing servers to reproduce bugs and edge cases.</li><li>For legal reasons, we cannot let the store reviewers access the production environment, only the testing ones.</li></ul><p>In this article, we describe the evolution of our solution to this problem.</p><h3>The requirements</h3><p>There are several requirements for the solution:</p><ol><li>The app should point to production API by default.</li><li>The testing environments’ URLs should not be hardcoded in the app.</li><li>The configuration change should be easy to do even for non-developers.</li></ol><p>The first requirement stems from the notion of “correct by default”. In other words, if the user does nothing, the app works with the API we want it to. This rules out making different builds for different environments (besides being a build process nightmare).</p><p>We also do not want the testing API URLs hardcoded in the app mainly because it would be impossible to add a new testing environment without making a new build of the app.</p><p>The ease of use is also necessary for two reasons. First, our testers switch environments often and we want to make the switch process as fast as possible. Second, if we wanted to make a test release among our employees (the majority of whom are non-technical), they need to be able to switch the environment without any advanced technical knowledge.</p><h3>The baseline solution</h3><p>The first approach to this problem we took was creating a special config file. This file would be placed on the device in a place where the app could detect and read it. This means the app’s files directory on Android and Documents directory on iOS. When the app detects the file with the proper values, it makes a &quot;secret&quot; developer tools section available where the user can make the switch using a picker.</p><p>This was relatively easy to implement using the wonderful <a href="https://github.com/itinance/react-native-fs">react-native-fs</a> package:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1f90f4728dae9d42f34c8cfbd8cbd9d7/href">https://medium.com/media/1f90f4728dae9d42f34c8cfbd8cbd9d7/href</a></iframe><p>First, we check whether a config file is present and then try to parse it as a JSON. A sample config file looks like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9bf464b5690c92c309b1cceef11e79b1/href">https://medium.com/media/9bf464b5690c92c309b1cceef11e79b1/href</a></iframe><p>Notice that the selectedEnv value corresponds to one of the keys of the baseUrls object. This allows us to craft config files with a particular environment already selected, so that the user doesn&#39;t have to make the switch manually. Moreover, it enables us to switch the app&#39;s API endpoints while not showing all the other developer tools at all (making the switch impossible) by not setting the developerTools to true!</p><p>The configFile methods are called in our configuration handling logic like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3c53c26d338965a9a13d249efb0c9831/href">https://medium.com/media/3c53c26d338965a9a13d249efb0c9831/href</a></iframe><p>We try to access the file and if it exists, we use its values to override the default settings. We also persist the last environment the user selected. This means they don’t have to set the environment every time they start the app.</p><p>This solution ticks the first two boxes. If there is no config file detected by the app, it defaults to the production. Also, the testing environments’ URLs are fully specified in the config file. This means adding a testing environment is as easy as updating the config file.</p><p>The ease of use aspect however leaves much to be desired, as putting the file in the correct place on the device is not so simple. On Android, this means getting the file into the device (be it by email or using a USB cable) and using the built in Files app to copy it into a directory with seemingly cryptic names several layers deep. On iOS, the process is opening iTunes, connecting the device using a cable and copying the config file using the <a href="https://support.apple.com/en-us/HT201301">File Sharing</a> functionality.</p><p>On both platforms, this is a lot of hustle and takes up to minutes to complete for an inexperienced user. After some brainstorming, an idea emerged — we’ll use <a href="https://en.wikipedia.org/wiki/QR_code">QR codes</a>!</p><h3>The user-friendly solution</h3><p>At first, the idea of adding the support of reading QR codes to a React Native app seemed extremely hard. Fortunately, there is a great package already available — <a href="https://github.com/moaazsidat/react-native-qrcode-scanner">react-native-qrcode-scanner</a> which is built on top of the incredible <a href="https://github.com/react-native-community/react-native-camera">react-native-camera</a>.</p><h3>Installing the packages</h3><p>To make things work you first need to follow the <a href="https://github.com/react-native-community/react-native-camera#getting-started">react-native-camera installation guide</a>. For us, <a href="https://github.com/react-native-community/react-native-camera#mostly-automatic-install-with-cocoapods">the iOS CocoaPods installation</a> went well, however we needed to take <a href="https://github.com/react-native-community/react-native-camera#android">the manual path</a> for Android to make it work.</p><p>After making sure your app compiles and runs with the react-native-camera package included, you can install the react-native-qrcode-scanner using npm or yarn. As it is implemented purely in JavaScript, there are no additional installation steps.</p><h3>Hiding the functionality</h3><p>For obvious reasons, we don’t want the regular users to know about this functionality, it is meant only for instructed developers and testers. Still, we need to have this feature accessible in production builds. This means we need to put it some “secret” place. There are several ways to achieve this, for example:</p><ol><li>Enabling the feature after a sequence of short and long taps on some element in the app (detecting this could be a nice exercise in <a href="https://en.wikipedia.org/wiki/Automata-based_programming">finite automata programming</a>)</li><li>Enabling the feature after a specific string was input in some unrelated field in the app</li><li>Enabling the feature after a shake gesture on a specific screen</li></ol><p>I’m not going to tell you the exact way we chose, this section was meant merely to point out you need to think about this when implementing similar feature.</p><h3>Using the scanner</h3><p>Having installed both packages and prepared the “secret” place, actually using the scanner is really easy.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/2d48ad21180649bc2c2306ea058f9237/href">https://medium.com/media/2d48ad21180649bc2c2306ea058f9237/href</a></iframe><p>As we can see, the InconspicuousComponent displays its normal contents and only when it receives the isDevModeEnabled prop set to true, it renders the QRCodeScanner as well, passing the handler received in props to it. In the handler, the QR contents are parsed from JSON and a basic validation of the contents is made. If the validation succeeds, the config is persisted into the config file on the device using the persistConfigFile function introduced earlier and the user is notified of the results by an <a href="https://facebook.github.io/react-native/docs/alert">Alert box</a>.</p><h3>Creating the QR codes</h3><p>The final thing to do is to create the QR code itself. There are many online generators available, we used <a href="https://www.the-qrcode-generator.com/">the-qrcode-generator.com</a>. Simply paste your config file contents in and download the code. The important thing here is to use the <strong>minified</strong> version of the JSON because we want to encode as few characters as possible. Again, there are many tools online to minify a JSON file or maybe your editor can do it as well.</p><p>Below is a QR code corresponding to the sample config file mentioned earlier:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*PtJLfpMSFrTBsd6dMU8oVw.png" /><figcaption>Sample QR code</figcaption></figure><p>You can then distribute the code to your testers along with the description of how to access the “secret” area. And that&#39;s it!</p><h3>Conclusion</h3><p>In this article, we discussed how to allow testers and other instructed user to change the environment of your React Native app. First, we described the basic solution and then illustrated how the process can be made much more user-friendly using QR codes.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7e87233a1af7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vzp-engineering/using-qr-codes-to-facilitate-testing-config-in-react-native-7e87233a1af7">Using QR codes to facilitate testing config in React Native</a> was originally published in <a href="https://medium.com/vzp-engineering">VZP Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Setting up Storybook in TypeScript/Sass environment with Webpack 4]]></title>
            <link>https://medium.com/vzp-engineering/setting-up-storybook-in-typescript-sass-environment-with-webpack-4-5a9d4587b397?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/5a9d4587b397</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[storybook]]></category>
            <category><![CDATA[webpack]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Fri, 10 Aug 2018 13:01:01 GMT</pubDate>
            <atom:updated>2018-08-10T13:01:01.941Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RT-8TwT5byeb4F0uObcBSQ.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/photos/Oaqk7qqNh_c">Patrick Tomasso</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure><p><a href="https://storybook.js.org/">Storybook</a> is a wonderful tool for developing UI components. It allows you to see your components in all their supported states by providing proper data to them. It supports quite a few libraries (Angular, Vue, React, and others). We have been using it for quite some time in our shared UI elements packages without issues. However, when we tried to add it to our Moje VZP Single Page App it was not exactly easy to make it work. In this article, we will describe the process of setting up the Storybook integration and the issues we encountered.</p><h3>Project stack</h3><p>The previous post on this blog <a href="https://medium.com/vzp-engineering/optimizing-bundle-sizes-of-react-single-page-application-in-typescript-1a0da9eea635">goes into great detail</a> about the Moje VZP project technology stack, so let’s just point out the relevant information here.</p><p>The app is written in <a href="https://reactjs.org/">React</a> using <a href="https://www.typescriptlang.org/">TypeScript</a>. The code is bundled using <a href="https://webpack.js.org/">Webpack 4</a> with TypeScript compiler <em>and</em> <a href="https://babeljs.io/">Babel</a>. The styles are written in <a href="https://sass-lang.com/">Sass</a> and are bundled by Webpack as well.</p><h3>Initial setup</h3><p>The recommended way to set Storybook up is by using their CLI utility — <a href="https://www.npmjs.com/package/@storybook/cli">@storybook/cli</a>. While this utility works well for “standard” projects, it has two issues that prevented us from using it:</p><ul><li>it uses <a href="https://www.npmjs.com/">npm</a> instead of <a href="https://yarnpkg.com/">yarn</a> that we use</li><li>it does not take neither TypeScript nor Sass into account</li></ul><p>Because of those two reasons we have to set Storybook up manually by loosely following the <a href="https://storybook.js.org/basics/guide-react/">slow start guide</a>.</p><p>The first step is to install Storybook itself by running yarn add -D @storybook/react @types/storybook__react. This adds the React version of Storybook and TypeScript type definitions for it (this allows us to write the stories in TypeScript, more on that later on).</p><p>Next, we add a script for starting the Storybook to our package.json:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3ea96c3bbd85089a372899178b3c90b1/href">https://medium.com/media/3ea96c3bbd85089a372899178b3c90b1/href</a></iframe><p>Finally, we create the config file .storybook/config.js in our app&#39;s root:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/33c0a60212d66995ffed2bc375232e7b/href">https://medium.com/media/33c0a60212d66995ffed2bc375232e7b/href</a></iframe><p>The file tells Storybook that the story files are located inside the app folder and their names end in .stories.tsx. The reason for not using the default file is that we think it is more clear when the stories are right next to &quot;their&quot; components rather than in some arbitrary folder. So for example for the Currency component specified in Currency.tsx there is now Currency.stories.tsx file with its stories right next to it.</p><p>Notice that the stories will be written in TypeScript. In a project with components written in TypeScript as well this makes writing the stories much easier — the prop types are checked therefore you are unlikely to pass nonsensical data to the component. This is especially useful with components that display complex data.</p><h3>Making TypeScript work</h3><p>Unfortunately, Storybook does not support TypeScript out of the box. There is a <a href="https://storybook.js.org/configurations/typescript-config">guide</a> for setting it up, however we ran into some issues using it.</p><p>The guide states that we need to extend the Storybook Webpack config in order to use TypeScript files. However, there are two issues with this:</p><ul><li>the current stable version of Storybook (3) uses Webpack 3, whereas we use Webpack 4</li><li>the guide suggests using awesome-typescript-loader, whereas we use ts-loader</li></ul><p>These two facts together mean that if we wanted to use ts-loader we already use in bundling our app (which is only compatible with Webpack 4), we would need to have two versions of ts-loader – one for Webpack 4 for bundling our app and one for Webpack 3 to use with Storybook. This is of course not desirable. Fortunately, there is an alpha version of Storybook 4 available that uses Webpack 4.</p><p>Therefore the first step to make the TypeScript work is to install the alpha version — yarn add -D @storybook/react@alpha. Next, we can create the .stories/webpack.config.js:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b15fa43b9a416591a333b5260406d069/href">https://medium.com/media/b15fa43b9a416591a333b5260406d069/href</a></iframe><p>As for the tsconfig.test.json, it extends our &quot;normal&quot; tsconfig.json and overrides some settings needed by Storybook (and Jest as well, hence the name test):</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/c7a115428297be75b77c6c53d4ab1509/href">https://medium.com/media/c7a115428297be75b77c6c53d4ab1509/href</a></iframe><p>As we can see, it uses the <a href="https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends">extend functionality</a> to override our standard configuration. It changes is the module system and tells TypeScript to compile JSX (our base config uses the esnext module system and does not compile JSX). This makes TypeScript output files Storybook can use.</p><p>This is all it takes to make TypeScript work (tested with @storybook/react@4.0.0-alpha.16).</p><h3>Making styles work</h3><p>As stated earlier, the styles for our app are written in Sass. This again requires additional configuration in Storybook.</p><p>First, we need to make sure Storybook knows how to process .scss files by updating .stories/webpack.config.js:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/65e5523a888306938c9bbabdd4d4e0fc/href">https://medium.com/media/65e5523a888306938c9bbabdd4d4e0fc/href</a></iframe><p>As we can see, the setup is similar to the TypeScript one — we add appropriate loaders and add the extension. Note that we did not add loader configuration for CSS as Storybook supports it by default and adding it here would cause errors. The getSassLoaderConfig function is used by our standard Webpack config as well and looks like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fbf508edfddf008b75629a322acd3b93/href">https://medium.com/media/fbf508edfddf008b75629a322acd3b93/href</a></iframe><p>The last thing to do is to include the styles in the stories. This can be done for all the stories at once by updating the .storybook/config.js with an import of the styles&#39; main file:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e9849b926ace76bbd058ee976e2499d5/href">https://medium.com/media/e9849b926ace76bbd058ee976e2499d5/href</a></iframe><h3>Writing components properly</h3><p>Having made Storybook work with our stack, the last thing is to write the stories themselves. There is a <a href="https://storybook.js.org/basics/guide-react/#write-your-stories">guide</a> on how to do it so we will not go into detail here.</p><p>The important thing is that you must write your components in a way that makes using them in Storybook possible. For us this meant splitting components using Redux into two as mocking Redux would be impractical.</p><p>Let’s use HotNews as an example:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/97852be90d7aff6541aa08917b03148f/href">https://medium.com/media/97852be90d7aff6541aa08917b03148f/href</a></iframe><p>This component downloads some news data and displays it. This works well, but we cannot write stories for it because it is connected to Redux which is not available in Storybook. The solution is to split HotNewsList into two components:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ba1e2f27bf6d9093a25028fb7144ee93/href">https://medium.com/media/ba1e2f27bf6d9093a25028fb7144ee93/href</a></iframe><p>The HotNews component no longer handles the data obtaining, just the data display. Thanks to this change it can now be used in Storybook, we just provide it the relevant data. There is no need for the complication of mocking Redux.</p><p>The logic behind getting the data from server and connecting to Redux was moved to HotNewsContainer which uses the HotNews internally and passes it the data from the server. Also, wherever we previously used HotNews in our app, we now must use HotNewsContainer instead.</p><h3>Summary</h3><p>In this article we’ve shown a way to make Storybook work in an environment with TypeScript, Webpack 4 and Sass. We also discussed how to write components in a way suitable for Storybook. Hopefully, this will inspire you, to try Storybook yourself, it really is worth it!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5a9d4587b397" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vzp-engineering/setting-up-storybook-in-typescript-sass-environment-with-webpack-4-5a9d4587b397">Setting up Storybook in TypeScript/Sass environment with Webpack 4</a> was originally published in <a href="https://medium.com/vzp-engineering">VZP Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Optimizing Bundle Sizes of React Single Page Application in TypeScript]]></title>
            <link>https://medium.com/vzp-engineering/optimizing-bundle-sizes-of-react-single-page-application-in-typescript-1a0da9eea635?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/1a0da9eea635</guid>
            <category><![CDATA[tree-shaking]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[webpack]]></category>
            <category><![CDATA[code-splitting]]></category>
            <category><![CDATA[typescript]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Fri, 27 Jul 2018 13:21:22 GMT</pubDate>
            <atom:updated>2018-07-30T05:49:51.409Z</atom:updated>
            <content:encoded><![CDATA[<h3>Optimizing Bundle Sizes of a React Single Page Application in TypeScript</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5oDOe111QZYBmVcY1rJQ9w.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/photos/DX5r6BNoWVE">Alexander Sinn</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure><p>If you are reading this, you have probably written some Single Page App. You probably also know that their size can get out of hand quickly. In this article we will discuss how we managed to improve the size of our main business to client app using several techniques including</p><ul><li>image optimization</li><li>dependency optimization</li><li>tree shaking</li><li>code splitting</li></ul><h3>The application</h3><p>The application is called Moje VZP (My VZP) and its main purpose is to allow our clients to get all the information about their health insurance they need. It is a React Single Page App written in TypeScript. We use Webpack to create the application bundles and transpile the code using TypeScript and Babel (because of the <a href="https://www.npmjs.com/package/babel-preset-env">babel-preset-env</a> that provides polyfills for older browsers). The styles use SCSS and Bootstrap. As for the images, there are a few SVG icons and one small PNG banner.</p><h3>The old state</h3><p>Before we started the size optimization efforts, the app consisted of three bundles:</p><ul><li>vendors.js – all the third-party packages (React, lodash, etc.)</li><li>app.js – all the application code</li><li>commons.js – all the CSS and images</li></ul><p>Below is the screenshot of <a href="https://www.npmjs.com/package/webpack-bundle-analyzer">webpack-bundle-analyzer</a> output (<a href="https://cdn.rawgit.com/no23reason/c49f1ed3b73431464dc0367c42258076/raw/395489442861ed51723de6f5eeb92f38a23d452d/before-optimization.html">interactive version</a>):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qIqDpTSdzqYpK0-jz7iqGQ.png" /><figcaption>Webpack bundles before optimization</figcaption></figure><p>As we can see the biggest parts of the vendors.js bundle are our @vzp/* packages with their dependencies – <a href="https://www.npmjs.com/package/cleave.js">cleave.js</a> (formatted input) and <a href="https://www.npmjs.com/package/react-intl-tel-input">react-intl-tel-input</a> (specialized phone number input). These total at 70.84 kB gzipped. Another big part is lodash (32.2 kB gzipped). We found ways to take these sizes down substantially (see <em>Tree Shaking</em> and <em>Lodash Optimization</em>).</p><p>The app.js bundle is relatively uninteresting, the only thing to note is that it contains the code of the entire app in a single bundle. This means every time the user accesses our app, they download the whole app even with parts they will not (or in some cases even <em>cannot</em>) use. We solved this as well (see <em>Code Splitting</em>).</p><p>Finally, the commons.js bundle contains mainly CSS and images. We haven&#39;t managed to make the CSS smaller yet (it uses Bootstrap for historical reasons, so we would need to rewrite it substantially), however we managed to trim a few kilobytes of the images (see <em>Image Optimization</em>).</p><h3>Image Optimization</h3><p>Let’s start with the most straight forward means to make the app payload smaller — optimizing image sizes. This can be achieved really easily in Webpack thanks to the wonderful <a href="https://www.npmjs.com/package/image-webpack-loader">image-webpack-loader</a>. This loader uses the <a href="https://github.com/imagemin/imagemin">imagemin</a> library to optimize various image formats during the Webpack build. Configuring this is as easy as running yarn add -D image-webpack-loader and updating webpack.config.js:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f228da4638d20759bd99991d47b64261/href">https://medium.com/media/f228da4638d20759bd99991d47b64261/href</a></iframe><p>We disable the image optimization in development mode to make the build as fast as possible when working on the app.</p><p>With this change, the image part of the commons.js bundle went from 7.9 kB gzipped to 5.85 kB gzipped. Not a huge win in absolute numbers, but considering it is more than 25% decrease for only a few lines of build script, this is a nice save.</p><h3>Tree Shaking</h3><p>The term <em>tree shaking</em> refers to the process of eliminating unused code by leaving out modules that are not imported anywhere. Without tree shaking, when you import a part of a package, the whole package is included as well. This can be seen on our @vzp/validatedform:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1EsBH0_rBza6vdRIoaXJug.png" /><figcaption>validatedform before tree shaking</figcaption></figure><p>Even though the whole application does not contain a single radio button or checkbox, both of those are included in the final bundle. This problem is manifested in several other packages.</p><p>To take advantage of the tree shaking functionality of Webpack, the app must conform to a few restrictions <a href="https://webpack.js.org/guides/tree-shaking/#conclusion">as stated in the docs</a>:</p><blockquote>[1] Use ES2015 module syntax (i.e. import and export).</blockquote><blockquote>[2] Add a “sideEffects” property to your project’s package.json file.</blockquote><blockquote>[3] Include a minifier that supports dead code removal (e.g. the UglifyJSPlugin).</blockquote><p>So that is what we did. First, we had to revise the way our project is built. Previously we used TypeScript to transpile our code to ES5 and Babel to handle the polyfills. We changed that so that TypeScript does only the type checking and Babel handles the transpilation and polyfills. In order to do that we updated our tsconfig.json:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f0edf1467f6b3f495f13248db027f3cb/href">https://medium.com/media/f0edf1467f6b3f495f13248db027f3cb/href</a></iframe><p>As we can see, TypeScript now transpiles the code to ES6 using the ESNext module system. The other three settings are necessary to make things work. Without &quot;moduleResolution&quot;: &quot;node&quot; the modules are resolved in a way incompatible with ESNext and both esModuleInterop and allowSyntheticDefaultImports improve interoperabilty at runtime. For example, they allow you to write import _ from &quot;lodash&quot; where previously you had to write import * as _ from &quot;lodash&quot; which is not permitted in ESNext.</p><p>The next step was to update .babelrc by setting &quot;modules&quot;: false in babel-preset-env options:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/312951a7477741d350e48ae15dcff4ff/href">https://medium.com/media/312951a7477741d350e48ae15dcff4ff/href</a></iframe><p>Doing so tells Babel to leave the modules alone in order to allow Webpack to access the imports and exports in the form it needs them to tree shake. As a side effect we observed a decrease in the overall bundle size just by setting this flag (even in projects without tree shaking). There is in fact an <a href="https://github.com/babel/babel-loader/issues/521">open issue</a> in the babel-loader repository to make this value the default.</p><p>These two changes make our app pass the first requirement — using the proper import/export syntax. To pass the second one, we needed to update our @vzp/* packages.</p><p>The update was twofold: first we updated their build process similarly to what we described above so that they are built as ESNext modules, then we added the &quot;sideEffects&quot;: false to their package.json files. Before doing so, all the packages were checked it they do not contain any side effect in order to avoid bugs. I am glad to say none of our packages contain side effects!</p><p>The third requirement was passed automatically as Webpack 4 uses Uglify in its production mode by default.</p><p>Now the @vzp/validatedform looks much better:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PD2FTE2XwvPueXVtEWsQIQ.png" /><figcaption>validatedform after tree shaking</figcaption></figure><p>The Radio, Checkbox and other unused input types are not included showing the tree shaking works!</p><h3>Lodash optimization</h3><p>With tree shaking enabled some of the third-party dependencies got smaller and more optimized. However, lodash remained the same. When researching the reason for this, we found out that the problem is in the way we import lodash functions:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/81a76ab000ed57036c7dcb59472b7087/href">https://medium.com/media/81a76ab000ed57036c7dcb59472b7087/href</a></iframe><p>In other words, you need to import specific functions from lodash for tree shaking to work. You could write the imports manually which however gets tedious fast and more importantly it takes missing <em>one</em> bad import and you are back to the whole lodash being included in your bundles.</p><p>Luckily, there is a better way — <a href="https://www.npmjs.com/package/babel-plugin-lodash">babel-plugin-lodash</a>. This plugin transforms the wrong imports into the correct ones automatically. All it takes is to yarn add -D babel-plugin-lodash and one update in .babelrc:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e73a29c9bb5c763f240baad946b75d7e/href">https://medium.com/media/e73a29c9bb5c763f240baad946b75d7e/href</a></iframe><p>Another step suggested on the plugin’s website is to use <a href="https://www.npmjs.com/package/lodash-webpack-plugin">lodash-webpack-plugin</a> as well to make lodash even smaller. This is achieved by removing some more advanced (and therefore rarely used) lodash features. It is straight-forward to set up: yarn add -D lodash-webpack-plugin and update webpack.config.js:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/86e5c871437b54302f677dbc0f20bf26/href">https://medium.com/media/86e5c871437b54302f677dbc0f20bf26/href</a></iframe><p>While the setup was easy, this plugin can get a little tricky to use. The problems arise when you actually use some of the “advanced” features in your code. This fact can go undetected — the Webpack build succeeds, the bundles get smaller, and then your app behaves weirdly or crashes. The plugin allows to opt-in to groups of features by setting the appropriate options in webpack.config.js, however it can be difficult to determine which feature sets you need. In our case the only one was flattening, because we use _.flow in several places. Just know that using this plugin can break your app if you are not careful!</p><h3>Code Splitting</h3><p>Code Splitting refers to the process of dividing your app bundle into several smaller ones each including a part of your app and only loading them when the user needs them. This leads to faster startup times and saves bandwidth. Also, thanks to <a href="https://en.wikipedia.org/wiki/HTTP/2">HTTP/2</a> multiple parallel requests are not an issue like they used to be.</p><p>The main challenge of effective code splitting is the question where to make the split. The most basic form of code splitting is the so-called route-centric one. It comes from the same logic “normal” webpages work — each route path has its own app code.</p><p>While route-centric splitting is better than nothing, there is an even more effective approach — component-centric splitting. The idea is that sometimes you want to be even more granular than just routes.</p><p>For example, our app has a whole section in the Reimbursement History part of the app where clients can issue claims for their health care reimbursements (for example when they feel their health care provider invoiced something they had not provided). We call this section ClaimForm. ClaimForm consists of a multi-step wizard with forms on every step. The problem is that most of our users do not use this feature (or at least not every time they use our app). Without code splitting they download the code for ClaimForm even though they are not going to use it.</p><p>To facilitate component-centric code splitting in React apps, the wonderful <a href="https://www.npmjs.com/package/react-loadable">react-loadable</a> package was created. It allows developers to dynamically import parts of their apps in a nice declarative way. There is a really detailed guide on the project&#39;s home page so we will not go in to all the details, let&#39;s just see it in action!</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/789fbb3c5ee789488c22585212331d2a/href">https://medium.com/media/789fbb3c5ee789488c22585212331d2a/href</a></iframe><p>As shown in the listing above, the only thing we need is to import the react-loadable package and use it to create a new component. This component is created by calling the Loadable function. Loadable takes a configuration object with several options. The most important ones are:</p><ul><li>loader - which component to make loadable</li><li>loading – which component to display while the loaded one is being loaded</li><li>render – how to render the loaded component (optional in JavaScript, mandatory in TypeScript)</li></ul><p>The first two options are pretty self-explanatory, the render is tricky, though. While according to the docs it is optional, you <em>must</em> specify it if your component is not the default export of its module (e.g. using export default). Because we do not use default exports for components (some of the reasons are mentioned in the great <a href="https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html">TypeScript Deep Dive book</a>, we must specify the render method.</p><p>The resulting component then takes the same props as the original one.</p><p>An attentive reader may have noticed the /* webpackChunkName: &quot;claim-form&quot; */ comment in the dynamic import statement. This is used by Webpack to name the split chunk with a human readable name. By default, these chunks are just numbered sequentially. To make this comment work, webpack.config.js must be updated:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6be200c17a92c5b1ba9660e479339050/href">https://medium.com/media/6be200c17a92c5b1ba9660e479339050/href</a></iframe><p>As for the build part, the code splitting uses dynamic import syntax (that import() statement) which Babel does not support by default. Hence, we need to install a <a href="https://www.npmjs.com/package/babel-plugin-syntax-dynamic-import">plugin for that</a> (yarn add -D babel-plugin-syntax-dynamic-import) and enable it in .babelrc:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/50ce4c2f5eadd3d8281ad7c1e357c7f0/href">https://medium.com/media/50ce4c2f5eadd3d8281ad7c1e357c7f0/href</a></iframe><p>And that is all the setup needed! From now on Webpack will generate separate chunks for our loadable components. What is even better it will also detect code shared by the chunks and emit chunks with the shared code as well.</p><p>The result is best seen on a picture. After splitting on routes (Dashboard, InsuranceHistory and ReimbursementHistory) as well as two forms (ActivationForm and ClaimForm) and one UX element (PeopleSelector) our bundles look like this (<a href="https://cdn.rawgit.com/no23reason/2966a89afd60be277eed370a7fa8c848/raw/9f61e34f09db3e0d549b0d3841f5bc4e1577b00d/after-optimizations.html">interactive version</a>):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*avOx-26DNCQxdB96pdMoAw.png" /><figcaption>Webpack bundles after optimization</figcaption></figure><p>As you can see, there are several new chunks for the components themselves as well as the parts shared by two or more of them. Note that for example claim-form.js also includes the react-intl-tel-input as it is the only component using it. This means that users that do not use the ClaimForm do not have to download this relatively big package anymore!</p><h3>Savings</h3><p>The main question is was it worth it? The answer is (unsurprisingly) yes! Below is a comparison of before and after:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/bc99ab0c023bbcbc4fad2d5d10f71e26/href">https://medium.com/media/bc99ab0c023bbcbc4fad2d5d10f71e26/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/31e7816002561a8eecbeab4a1edd9aa8/href">https://medium.com/media/31e7816002561a8eecbeab4a1edd9aa8/href</a></iframe><p>The total savings are 47.35 kB gzipped which is a roughly 15 % decrease. More importantly though, when users initially open our app (on the Dashboard route), they only download 159.47 kB gzipped which is less than a half of the old payload!</p><p>You can explore the bundle visualizations yourself — <a href="https://cdn.rawgit.com/no23reason/c49f1ed3b73431464dc0367c42258076/raw/395489442861ed51723de6f5eeb92f38a23d452d/before-optimization.html">before optimizations</a> and <a href="https://cdn.rawgit.com/no23reason/2966a89afd60be277eed370a7fa8c848/raw/9f61e34f09db3e0d549b0d3841f5bc4e1577b00d/after-optimizations.html">after optimizations</a> — to see what really changed.</p><h3>Further work</h3><p>The steps discussed here are not exhaustive, there is still room for improvement. The biggest opportunity to further slim down the bundles is to collocate the styles with the components (be it using CSS-in-JS or “normal” imports) instead of using one big CSS file. This would enable the code splitting part of Webpack to handle the styles just as effectively as the scripts.</p><h3>Summary</h3><p>In this article we’ve shown some of the techniques you can use to make your Single Page Apps smaller and more effective. On a real project we illustrated the impact these can have in practice.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1a0da9eea635" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vzp-engineering/optimizing-bundle-sizes-of-react-single-page-application-in-typescript-1a0da9eea635">Optimizing Bundle Sizes of React Single Page Application in TypeScript</a> was originally published in <a href="https://medium.com/vzp-engineering">VZP Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Encouraging proper API response handling on code level]]></title>
            <link>https://medium.com/vzp-engineering/encouraging-proper-api-response-handling-on-code-level-5e4c00b13d0b?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/5e4c00b13d0b</guid>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[functional-programming]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Fri, 13 Jul 2018 14:15:05 GMT</pubDate>
            <atom:updated>2018-07-13T14:15:05.857Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*A9Z7yemZS9liCQcKunNl_w.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/photos/naQdcC4nVgA">Victor Zambrano</a> on <a href="https://unsplash.com/">Unspash</a></figcaption></figure><p>If you ever developed any application that communicates with a REST API, you probably know that handling the responses can be a bit tricky. It is often easy to forget to handle for example error states. In this article, we’ll show an approach that we devised to make forgetting this much less probable. The solution described uses TypeScript, but the core concept should be usable in other languages as well.</p><h3>Previous solution</h3><p>Not long after we started developing React Single Page apps, we found out that there are several things to keep track of for every REST API resource:</p><ul><li>whether there is a request pending (in other words if we are currently waiting for a server response)</li><li>whether there was an error last time we tried to call the API endpoint</li><li>when the last change of this object happened (usually the time of the last response)</li><li>the contents of the last successful response</li></ul><p>For this, we created a small library called loadable (this was before the popular <a href="https://github.com/jamiebuilds/react-loadable">react-loadable</a> pacakge was published, so the name clashes now). Its main purpose was to facilitate and unify representations of REST resources. The core object called Loadable can be seen bellow:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9cd81ae7b9b5f759ce4303c51d78c1ef/href">https://medium.com/media/9cd81ae7b9b5f759ce4303c51d78c1ef/href</a></iframe><p>As we can see, it has four properties — one for each thing we want to track mentioned above. It uses a generic parameter for the data property to allow representation of any REST resource in a type-safe way.</p><p>There was also a function for creating the instances that handled the timestamp assignment:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/db0ae206da7ec612f7c480f0657eee79/href">https://medium.com/media/db0ae206da7ec612f7c480f0657eee79/href</a></iframe><p>There is nothing wrong with this representation itself, in fact it is used as is in the new approach (see below). The problem is, there is no standard way to consume this object that would enforce that the consumer handles all the possible states the object can be in. Most of the time, there was code like this when handling Loadable instances in React code:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/10dc286e830d1d573ef036c4319f59b7/href">https://medium.com/media/10dc286e830d1d573ef036c4319f59b7/href</a></iframe><p>This was mostly fine, however, there are two main problems with this:</p><ol><li>forgetting to handle one of the states</li><li>handling of state combinations gets really messy</li></ol><p>It is easy to forget to handle one of the states (for us it was the error most of the time). This leads to suboptimal UX, because when a REST request fails, the app seems to not work at all.</p><p>Also, what if we wanted to display the previously received data <em>as well as</em> the loading indicator (for example when the user explicitly reloads the resource)? The logic gets really complex in those cases, and is super easy to make an error in.</p><p>The new approach aims to alleviate both of these issues.</p><h3>Current solution</h3><p>Recently I finally read the brilliant <a href="https://mostly-adequate.gitbooks.io/mostly-adequate-guide/content/">Professor Frisby’s Mostly Adequate Guide to Functional Programming</a>. Despite its wacky title, it is a really useful explanation of various functional programming concepts. You should definitely check it out if you haven’t already. <a href="https://mostly-adequate.gitbooks.io/mostly-adequate-guide/content/ch08.html">Chapter 8</a> discusses various containers and how they can help you with for example error handling. This inspired me to revise our loadable package and attempt to use the approaches described to fix our problems.</p><p>It turns out, both of the problems mentioned above can be solved by a simple form of “pattern matching”. I put it in quotes, because it is no <a href="https://en.wikipedia.org/wiki/Pattern_matching">pattern matching</a> per se, it shares the basic idea with it, though.</p><p>Instead of the createLoadable function, there is now a loadable utility object that looks like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3a6c91bd6648c00b774973a59e4f96b5/href">https://medium.com/media/3a6c91bd6648c00b774973a59e4f96b5/href</a></iframe><p>The name of the instance creating method (of) is inspired by the Guide, but it does the same thing as the createLoadable function used to. The interesting bit is the merge method:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/130a800c7277c01e7201e7f46b6dc2d0/href">https://medium.com/media/130a800c7277c01e7201e7f46b6dc2d0/href</a></iframe><p>As we can see, the match method uses a configuration object of the LoadablePattern&lt;T&gt; type. This type consists of handlers of all the states that the Loadable instance can be in.</p><p>The important thing to notice is that some of the handlers are marked as required (more precisely not marked as optional with the ?). This means that every caller of the merge method <em>has to</em> provide handlers for the basic states: loading in progress, data retrieved, error occurred, and the default empty state (when the resource has not been requested yet for the first time). Thus, we solve the first problem, if using the merge method, all the basic states are handled.</p><p>The second problem is solved by encapsulating the decision tree into one place. The caller can also specify more nuanced handlers. For example loadingWithData which represents a situation when we already had some data but are loading it again – hence we show the current data as well as the loading indicator.</p><p>The matching is opinionated regarding the priority in which the cases are matched. This is necessary as TypeScript does not have proper pattern matching (<a href="https://github.com/tc39/proposal-pattern-matching">yet</a>) which would allow the consumer to define the priority themselves using the order of the clauses. For our use cases this is fine though, as it means we handle the diferent states consistently across the app.</p><p>Using these methods is then as easy as:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/04ebe41e2f8234288864557c9929dc76/href">https://medium.com/media/04ebe41e2f8234288864557c9929dc76/href</a></iframe><p>As we can see, it allows us to extend the initial example by providing loadingWithData handler while retaining its previous functionality.</p><h3>Design decisions</h3><p>Observant reader might have two questions about this implementation:</p><ol><li>Why is the merge method curried?</li><li>Why is the merge method implemented as an instance method of the Loadable type?</li></ol><p>The reason for the currying is illustrated in the usage example above. We can declare the matcher once, instead of doing it on every render. This keeps the render method as lightweight as possible.</p><p>As to why the Loadable is not a class with merge as its instance method (and of as a static method) similarly to for example <a href="https://mostly-adequate.gitbooks.io/mostly-adequate-guide/content/appendix_b.html#either">Either</a>, the main reason is serializability.</p><p>Most of our TypeScript applications are React apps that use <a href="https://github.com/reduxjs/redux">Redux</a> as the store management solution (though I really want to look into <a href="https://github.com/mobxjs/mobx">MobX</a> soon). All of the Loadable instances we use are therefore stored in a Redux store. As you may know it is really beneficial if all the data stored in the store is serializable. This unfortunately means we cannot use the &quot;proper&quot; class approach easily.</p><p>If we were to use loadable in some other environment, I would definitely go for the class approach, as it avoids the Loadable/loadable dichotomy.</p><h3>Summary</h3><p>In this article we’ve shown how to enforce a certain practice on code level at compile time using functional approaches. Hopefully, this will inspire you to solve some of your code problems in a similar way.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5e4c00b13d0b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vzp-engineering/encouraging-proper-api-response-handling-on-code-level-5e4c00b13d0b">Encouraging proper API response handling on code level</a> was originally published in <a href="https://medium.com/vzp-engineering">VZP Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Creating accessible progress indicator in React]]></title>
            <link>https://medium.com/vzp-engineering/creating-accessible-progress-indicator-in-react-1ae7eca76633?source=rss-1b1e4f053e40------2</link>
            <guid isPermaLink="false">https://medium.com/p/1ae7eca76633</guid>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[Dan Homola]]></dc:creator>
            <pubDate>Fri, 29 Jun 2018 12:01:02 GMT</pubDate>
            <atom:updated>2018-07-25T20:26:12.955Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NvGn7X6U2UGbySN1vYxx2w.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/photos/w0XNdcZxJwg">Matt Artz</a> on <a href="https://unsplash.com">Unspash</a></figcaption></figure><p>When creating our registration application our UX designer came up with the following design (screenshot taken from our storybook):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/1*jM3DD0HebzJparQICpT_Bg.png" /></figure><p>It was relatively easy to implement using React, so we wrote it and forgot about it.</p><p>Several weeks later, we submitted the alpha version of the app to our accessibility testing consultant (as we do with all our apps). The results were that among other minor accessibility problems, the most serious was that the progress indicator (we call it thermometer) is completely inaccessible to screen readers. Not invisible, even worse — annoying, because when read using a screen reader it produced seemingly irrelevant words.</p><p>The accessibility problems can be split into these points:</p><ul><li>There was no indication this component represents a progress display</li><li>The circles were not numbered and labelled in any way</li><li>The lines connecting the circles did not provide information about how much of the way they were filled</li><li>There was no explanation what A***** D*** means (this was explained in the rest of the app, so a seeing users could easily read it, visually impaired users however could not)</li></ul><p>We will discuss how we fixed all these problems in the rest of this article.</p><h3>Declaring the semantic meaning</h3><p>First, let’s focus on how to declare that our component is a multi step progress indicator. We followed our consultant’s recommendation: The whole component is an ordered list where the list items consist of the circle and optionally the connecting line. Bellow there is the source code of the main component:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/984326c1781f00b9c7cab6c197cbd636/href">https://medium.com/media/984326c1781f00b9c7cab6c197cbd636/href</a></iframe><p>As we can see, it renders an ordered list with a descriptive <a href="https://www.w3.org/TR/wai-aria-1.1/#aria-label">aria-label attribute</a> that tells the user what it represents.</p><p>The list takes two props: items that specify name, icon and other properties of the individual steps, and position – two numbers that indicate which step is currently active and what is the percentual progress to the next one.</p><h3>Rendering the circles</h3><p>Next, let’s see how the Segments (i.e. list items) are rendered. Bellow is the code for the Segment component:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0c01d52210ee4e85357da14c754364a8/href">https://medium.com/media/0c01d52210ee4e85357da14c754364a8/href</a></iframe><p>The Segment component renders one circle and if it is no the last one, it renders the connecting line as well. The important thing here is that the li element is marked as aria-current when appropriate. This ensures that screen reader reads the current step like</p><blockquote><em>Account, current step</em></blockquote><p>ensuring the user knows what the current step is.</p><p>Other than that, the component is relativelly straight-forward, it computes some derived props for the Point and Line components (see bellow) based on the current position on the thermoeter.</p><p>The Point component represents the circle on the thermometer. It has a name, an icon and status, that indicates whether it is the current step, step already completed or one that is still to be visited. The whole code is listed bellow:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/35a783101c53be4261af18d6b54ca04a/href">https://medium.com/media/35a783101c53be4261af18d6b54ca04a/href</a></iframe><p>Point renders a div with some styling that ensures it is a circle of the right color and size depending on the status. Active step is larger than inactive for example.</p><p>Inside of the div, there are two spans – one for screen readers only and one for displays (hidden from screen readers using the <a href="https://www.w3.org/TR/wai-aria-1.1/#aria-hidden">aria-hidden attribute</a>). The reason for this duplicity is that we needed to provide additional description for the screen reader users. In our case it was a note that the name of the person being registered is redacted and therefore is spelled A***** D*** instead of Arthur Dent.</p><p>You may wonder why we did not simplify this to something like</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f001a9b6e632a41221acd39d80d17513/href">https://medium.com/media/f001a9b6e632a41221acd39d80d17513/href</a></iframe><p>While this would work relatively well, the screen reader would read the two parts separately. In other words the user would have to press a button to hear props.description as well. The way we wrote it, both strings get read at once.</p><h3>Rendering the connecting lines</h3><p>The last part we haven’t shown are the lines connecting the circles. This is the responsibility of the Line component:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f1242a27353ed597f559e4533b523215/href">https://medium.com/media/f1242a27353ed597f559e4533b523215/href</a></iframe><p>The visual part of its markup is simple: just a div contaning two overlapping divs that emulate the partially filled progress bar. It&#39;s the ARIA part that&#39;s interesting!</p><p>As we can see, the main div has several ARIA attributes, so let&#39;s explain those one by one.</p><p>The role attribute declares what the component represents semantically – in this case a progressbar. There are quite a few <a href="https://www.w3.org/TR/wai-aria-1.1/#roles_categorization">roles available</a> in the WAI-ARIA standard.</p><p>We used aria-label here to give name to the progress bar. In this case the title of the related step is used. This ensures the user knows which step the progress bar corresponds to.</p><p>Finally, the aria-valuenow specifies the curernt progress value while aria-valuemin and aria-valuemax specify the lowest and highest possible values. These are used by the screen readers to compute the percentual progress of the progress bar. This means that somthing like</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/19d1036ee9f4d5d13b7d1c0a2a6567bf/href">https://medium.com/media/19d1036ee9f4d5d13b7d1c0a2a6567bf/href</a></iframe><p>would be read as</p><blockquote><em>50 percent</em></blockquote><h3>Conclusion</h3><p>On a simple real life example we’ve shown how to make React components accessible to visually impaired users and hopefully inspired you to revise your own components with this perspective in mind. After all, accessible components are better components.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1ae7eca76633" width="1" height="1" alt=""><hr><p><a href="https://medium.com/vzp-engineering/creating-accessible-progress-indicator-in-react-1ae7eca76633">Creating accessible progress indicator in React</a> was originally published in <a href="https://medium.com/vzp-engineering">VZP Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>