<?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:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Krister's coding adventures]]></title><description><![CDATA[Snippets and tips that the internet is lacking from a coder still learning.]]></description><link>https://code.krister.ee/</link><image><url>https://code.krister.ee/favicon.png</url><title>Krister&apos;s coding adventures</title><link>https://code.krister.ee/</link></image><generator>Ghost 3.35</generator><lastBuildDate>Wed, 01 Apr 2026 06:41:34 GMT</lastBuildDate><atom:link href="https://code.krister.ee/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[New Project Commands for new Elixir Phoenix LiveView project in 2023]]></title><description><![CDATA[<p>These are my most common cli commands to start a new project with Elixir using Phoenix LiveView (and of course Ecto).</p><p>Phoenix 1.7 is still a Release Candidate while being used here so things may change. Will keep this updated.</p><h1 id="tldr">TLDR</h1><pre><code>mix local.hex
mix archive.install hex phx_</code></pre>]]></description><link>https://code.krister.ee/new-project-commands-for-new-elixir-phoenix-liveview-project/</link><guid isPermaLink="false">63c51aa8c1d3e80f1f871f48</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Mon, 16 Jan 2023 11:41:41 GMT</pubDate><content:encoded><![CDATA[<p>These are my most common cli commands to start a new project with Elixir using Phoenix LiveView (and of course Ecto).</p><p>Phoenix 1.7 is still a Release Candidate while being used here so things may change. Will keep this updated.</p><h1 id="tldr">TLDR</h1><pre><code>mix local.hex
mix archive.install hex phx_new 1.7.0-rc.2
mix phx.new app_name --binary-id
mix phx.gen.auth Accounts User users</code></pre><p>Plus do the UTC Datetime part - there's no TLDR variant of it.</p><h1 id="update-elixir-erlang">Update Elixir / Erlang</h1><p>I use <code><a href="https://github.com/asdf-vm/asdf">asdf</a></code> to handle versions.</p><pre><code>asdf list all elixir                          # find latest version
asdf install elixir 1.14.3-otp-25             # install latest
asdf global elixir 1.14.3-otp-25              # use latest

asdf list all erlang
asdf install erlang 25.2
asdf global erlang 25.2

mix local.hex                                 # update hex
mix archive.install hex phx_new 1.7.0-rc.2    # phx.new for phoenix 1.7</code></pre><h1 id="new-liveview-project">New LiveView Project</h1><pre><code>mix phx.new app_name --binary-id</code></pre><p><code>--binary_id</code> makes Ecto use UUID instead of number id's by default.</p><p>Since Phoenix 1.7 TailwindCSS is already included, so no extra steps for that. </p><p>Then follow guide in console and <code>git commit -am "Phoenix boilerplate"</code>.</p><h1 id="authentication">Authentication</h1><p>Phoenix has a handy command to generate a simple register / login flow. This generates the code and doesn't import new deps. That's great since we can then change the behaviour to how we want. Like to use magic link for example.</p><pre><code>mix phx.gen.auth Accounts User users</code></pre><h1 id="use-utc-datetime-everywhere">Use UTC Datetime everywhere</h1><blockquote>You should use <code>:utc_datetime</code> unless you have a reason not to. The reason why Ecto’s <code>timestamps()</code> are naive by default is backwards compatibility. Since they are always UTC anyway, you should use <code>:utc_datetime</code> for them (and I believe the default will be changed to that in the future).</blockquote><p>That's from <a href="https://elixirforum.com/t/difference-in-between-utc-datetime-and-naive-datetime-in-ecto/12551/2?u=kristerv">Elixir Forum</a>.</p><p>Ecto uses <code>NaiveDatetime</code> by default so lets change it.</p><pre><code># config/config.exs
config :app, App.Repo, migration_timestamps: [type: :utc_datetime]</code></pre><p>And every Schema file you create must use:</p><pre><code>@timestamps_opts [type: :utc_datetime]</code></pre><p>Since we just generated schemas, let's change their datetimes:</p><pre><code># lib/app/accounts/user_token.ex

defmodule App.Accounts.UserToken do
  use Ecto.Schema
  [...]

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  @timestamps_opts [type: :utc_datetime]           # add this line
  schema "users_tokens" do
    field :token, :binary
  
  [...]</code></pre><pre><code># lib/boocs/accounts/user.ex

defmodule App.Accounts.User do
  use Ecto.Schema
  [...]
  @timestamps_opts [type: :utc_datetime]                # add this
  schema "users" do
    [...]
    field :confirmed_at, :utc_datetime                  # change this
    timestamps()
  end
  [...]</code></pre><p>We could use a custom Schema macro to do the Schema change for us, but then we'd still have to remember to use it when generating files, so doesn't seem like we can get over manual work for now.</p><h1 id="staying-up-to-date">Staying up-to-date</h1><p>Since we're using generators we'd like to know what changes new versions of the generators come with. For Phoenix itself you can use the official upgrade guide or <a href="https://phoenixdiff.org">https://phoenixdiff.org </a> to see what's new.</p><p>You may also generate a new project, copy files on top of your current one and then just check the diff. I would not suggest making changes this was as some detail may be missed. Instead look at the diff and make changes to your project manually.</p><h1 id="bonus-libraries">Bonus: Libraries</h1><p>These are the libraries that I use most often. Not all of them evey project, but it's a good list to check.</p><!--kg-card-begin: markdown--><ul>
<li><a href="https://hexdocs.pm/httpoison">httpoison</a> for making requests</li>
<li><a href="https://hexdocs.pm/decimal">decimal</a> for accurate calculations</li>
<li><a href="https://hexdocs.pm/credo">credo</a> helper for clean code</li>
<li><a href="https://github.com/esl/gradient">gradient</a> gradual typechecker</li>
<li><a href="https://hexdocs.pm/sobelow">sobelow</a> security-focused static analysis tool</li>
<li><a href="https://hexdocs.pm/shorter_maps">shorter_maps</a> destruct maps</li>
<li><a href="https://hexdocs.pm/typed_struct">typed_struct</a> write types with ease</li>
<li><a href="https://hexdocs.pm/typed_ecto_schema">typed_ecto_schema</a> type writer for ecto schemas</li>
<li><a href="https://hexdocs.pm/timex">timex</a> easier datetime formatting</li>
<li><a href="https://hexdocs.pm/kaffy">kaffy</a> quick admin dashboard, like PHPMyAdmin</li>
<li><a href="https://hexdocs.pm/ecto_shorts">ecto_shorts</a> simpler Ecto API</li>
<li><a href="https://hexdocs.pm/ex_machina">ex_machina</a> generate DB items for testing</li>
<li><a href="https://hexdocs.pm/ex_money">ex_money</a> don't roll your own currency</li>
<li><a href="https://hexdocs.pm/oban">oban</a> jobs queue</li>
</ul>
<!--kg-card-end: markdown--><p>Bonus 2: I made a <a href="https://hexdocs.krister.ee/">search engine for hexdocs!</a></p>]]></content:encoded></item><item><title><![CDATA[AB Testing in Elixir, Phoenix]]></title><description><![CDATA[<p>Here's how you can split traffic into multiple variants, while keeping that user on the same page on revisits. Nothing fancy. How to get stats to analytics isn't covered.</p><h3 id="direct-all-routes-to-a-single-function">Direct all routes to a single function</h3><p>Optional. I like my static page traffic handled by a single function.</p><p><code>lib/platform_</code></p>]]></description><link>https://code.krister.ee/ab-testing-in-elixir-phoenix/</link><guid isPermaLink="false">616970f2c1d3e80f1f871e72</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Fri, 15 Oct 2021 12:32:24 GMT</pubDate><content:encoded><![CDATA[<p>Here's how you can split traffic into multiple variants, while keeping that user on the same page on revisits. Nothing fancy. How to get stats to analytics isn't covered.</p><h3 id="direct-all-routes-to-a-single-function">Direct all routes to a single function</h3><p>Optional. I like my static page traffic handled by a single function.</p><p><code>lib/platform_web/router.ex</code></p><pre><code class="language-elixir">for path &lt;- PlatformWeb.HomeController.available_paths() do
    get "/#{path}", HomeController, :static_page
end</code></pre><h3 id="divide-the-traffic">Divide the traffic</h3><p>I'll show you the whole file and comment in between.<br><code>lib/platform_web/controllers/home_controller.ex</code></p><pre><code class="language-elixir">defmodule PlatformWeb.HomeController do
    use PlatformWeb, :controller</code></pre><p>I don't want a catch-all routing disaster so I've defined available routes manually. We could map the actual templates here, but then the AB testing templates are available too. Manual is safe.</p><pre><code class="language-elixir">def available_paths do
    ["/", "/contact", "projects", "pricing", "about"]
end</code></pre><p>The "handle any route" function just takes the requested page, asks the <code>ab_test()</code> function to adjust the actual file rendered and then renders it.</p><pre><code class="language-elixir">def static_page(conn, _params) do
    path = Enum.at(conn.path_info, 0, "index")
    {conn, page} = ab_test(conn, path)
    render(conn, page, %{})
end</code></pre><p>The <code>ab_test()</code> function mainly deals with session memory. If this user has already seen a page, then show the same one again. Lets not get our visitors confused. If this page has not yet been seen, choose a <code>random_variation()</code>.</p><pre><code class="language-elixir">  defp ab_test(conn, requested_path) do
    memory = get_session(conn, :ab_test_memory) || %{}
    page = Map.get(memory, requested_path) || random_variation(requested_path)
    new_memory = Map.put(memory, requested_path, page)
    conn = put_session(conn, :ab_test_memory, new_memory)
    {conn, page}
  end</code></pre><p>Choosing a random template was easy after figuring out the View actually holds all of the filenames.</p><pre><code class="language-elixir">  defp random_variation(page) do
    PlatformWeb.HomeView.__templates__()
    |&gt; elem(2)
    |&gt; Enum.filter(&amp;String.starts_with?(&amp;1, page))
    |&gt; Enum.random()
  end
end</code></pre><p>And voila. Pretty basic, once you have the template list at hand. If you've also implemented AB Testing, but in some way cooler manner - let me know.</p>]]></content:encoded></item><item><title><![CDATA[Errors not logging in Phoenix]]></title><description><![CDATA[<p>My user got an 'Internal Server Error' at a specific URL. I check the server logs - nothing. This is not the first time this has happened. So I spend a whole day looking for a solution and chatting about it. I try out my usual tricks like turning on</p>]]></description><link>https://code.krister.ee/errors-not-logging-in-phoenix/</link><guid isPermaLink="false">611a4f62c1d3e80f1f871e3e</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Mon, 16 Aug 2021 12:18:28 GMT</pubDate><content:encoded><![CDATA[<p>My user got an 'Internal Server Error' at a specific URL. I check the server logs - nothing. This is not the first time this has happened. So I spend a whole day looking for a solution and chatting about it. I try out my usual tricks like turning on <code>level: :debug</code> for logging. Sometimes Ecto errors are hidden because of that.</p><p>Curiously sometimes the error shows up. But after a hard refresh on the browser - again nothing. Just </p><pre><code class="language-bash">[info] GET /learn
[info] Sent 500 in 2ms</code></pre><p>Where's the error?</p><h2 id="solution">Solution</h2><p>I was advised to use Sentry, so I dug in and tried to figure out what they do differently. Their elixir integration is open-source so I found <a href="https://github.com/getsentry/sentry-elixir/blob/02a2cc31c7444af88d079f7c32a789b9b1b4e6f1/lib/sentry/plug_capture.ex">the plug that's responsible for capturing errors</a> and implemented that into my own app.</p><p>Amazingly it actually logs the hidden error. So this time the problem was actually Phoenix Endpoint not handling its error properly.</p><p>Here's my module.</p><pre><code class="language-elixir">defmodule PlatformWeb.EndpointErrorCatcher do
  @moduledoc """
  Since Phoenix doesn't capture all errors we've got to try-catch them ourselves. I have no idea how Phoenix is meant to be working in this regard, but I just had an invisible error in prod.
  Module is ripped from https://github.com/getsentry/sentry-elixir/blob/02a2cc31c7444af88d079f7c32a789b9b1b4e6f1/lib/sentry/plug_capture.ex
  #### Usage
  In a Phoenix application, it is important to use this module before
  the Phoenix endpoint itself. It should be added to your endpoint.ex:
      defmodule MyApp.Endpoint
        use EndpointErrorCatcher
        use Phoenix.Endpoint, otp_app: :my_app
        # ...
      end
  In a Plug application, it can be added below your router:
      defmodule MyApp.PlugRouter do
        use Plug.Router
        use EndpointErrorCatcher
        # ...
      end
  """
  defmacro __using__(_opts) do
    quote do
      @before_compile PlatformWeb.EndpointErrorCatcher
    end
  end

  defmacro __before_compile__(_) do
    quote do
      defoverridable call: 2

      def call(conn, opts) do
        try do
          super(conn, opts)
        rescue
          e in Plug.Conn.WrapperError -&gt;
            stack =
              e.stack
              |&gt; List.first()
              |&gt; elem(3)
              |&gt; then(fn [file: file, line: line] -&gt; "#{file}:#{line}" end)

            Logger.error("EndpointWrapperError at #{stack}: #{e.reason.message}")
            Plug.Conn.WrapperError.reraise(e)

          e -&gt;
            stack =
              __STACKTRACE__
              |&gt; List.first()
              |&gt; elem(3)
              |&gt; then(fn [file: file, line: line] -&gt; "#{file}:#{line}" end)

            Logger.error("EndpointUnknownError at #{stack}: #{e.reason.message}")

            :erlang.raise(:error, e, __STACKTRACE__)
        catch
          kind, reason -&gt;
            message = "Uncaught #{kind} - #{inspect(reason)}"
            stack = __STACKTRACE__
            Logger.error(message, stacktrace: stack)
            :erlang.raise(kind, reason, stack)
        end
      end
    end
  end
end</code></pre><p>It logs the file:line and reason for error.</p><p>Use it as per instructed in <code>@moduledoc</code>.</p>]]></content:encoded></item><item><title><![CDATA[A code editor for schools.]]></title><description><![CDATA[<p>Today I get to experience success. My students passed the final test with super high scores. They rated the programming course very highly. There were problems of course - a lot of them. But the core concept held up. My original teaching method seems to hold water.</p><p>Lectures are super</p>]]></description><link>https://code.krister.ee/a-code-editor-for-schools/</link><guid isPermaLink="false">60c3bd92c1d3e80f1f871d57</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Fri, 11 Jun 2021 20:12:59 GMT</pubDate><media:content url="https://code.krister.ee/content/images/2021/06/2021-06-11_23-14-12.png" medium="image"/><content:encoded><![CDATA[<img src="https://code.krister.ee/content/images/2021/06/2021-06-11_23-14-12.png" alt="A code editor for schools."><p>Today I get to experience success. My students passed the final test with super high scores. They rated the programming course very highly. There were problems of course - a lot of them. But the core concept held up. My original teaching method seems to hold water.</p><p>Lectures are super boring. The opposite of that is giving students the tools and letting them figure it out. In programming at least, this is actually possible. Give a goal, show them the tools, see what happens.</p><p>Answer questions, but don't explain before they ask. Give them hints, but only once they're already struggling with the issue. Help them across, but only after they've tried enough.</p><p>I taught programming to 17 students using my own invented platform for a full week.</p><p>The student is given instructions and the code (blue in the top image), but the code is in the wrong order. This hinting system is very primitive, it'll get better. But for now, it actually worked pretty well.</p><p>The student can always check out the Goal tab, where the task is finished already. It's a visual explanation of the current task. I'd say the students didn't really know what the task was really about most of the time, but they did learn syntax and logic without me having to explain it in the standard lecture format.</p><p>Every day was a new project. A homepage, a chat app, a game, a sophisticated website design. By the end of the day the code was pretty messy and many didn't finish, so starting from scratch every day was a great trick. These projects however were much more interesting than the usual console based Python stuff.</p><p>Any code they wrote was immediately shown on the right. This is immensely important to create relationships between the abstract text that is code and the concrete result that is a familiar app or website.</p><p>The most amazing part of all of this is that it actually worked! Friday's tests proved that they actually learned some important concepts in programming. And even though the projects were super difficult and I could see the students losing motivation because of it - we made it to the other side and many of them thought it was the best thing they've done all year.</p><p>What an amazing feeling. I've been teaching programming for a long time to university students and grownups. Somehow school kids have a different vibe. It feels more gratifying. Perhaps it's because they aren't looking to get a job. They're here to have fun and learn cool stuff. That's the way I like to learn things myself.</p><p>This was a paid gig btw. Teachers salary is not something a programmer will jump for joy at. But from a startup perspective it's the ultimate validation.</p><p>Students, teachers, head of school, parents - they all want programming to be in schools. There just aren't enough programmers in schools. Mostly because the salary is much lower and the stress is higher. This can be solved with a platform that automates the teachers job. It has courses pre-made and well designed. Guaranteed to be fun and low-stress for both the teacher and student. Then anyone can teach programming.</p><p>I'm going to solve this problem. Programming is going to reach schools finally. "Read, Write, Code" may be overly hyped for some. For me, it's a matter of principle. Not everyone has to become a programmer. But everyone should know how to create software, not only consume it.</p><p>There is still <em>a lot</em> to do. This is barely the beginning.</p>]]></content:encoded></item><item><title><![CDATA[Phoenix 500 with no errors (Elixir)]]></title><description><![CDATA[<p>I'm deploying LiveView for the first time and what a strange sight. Browser says <code>Internal server error</code>, yet server log has no error. I deploy again with <code>:debug</code> logger level ( <code>config :logger, level: :debug</code> ).</p><pre><code class="language-Elixir">Started Koodikool Platform.
[info] Running PlatformWeb.Endpoint with cowboy 2.8.0 at :::4010 (http)
[info]</code></pre>]]></description><link>https://code.krister.ee/phoenix-500-with-no-errors/</link><guid isPermaLink="false">607296f3c1d3e80f1f871d20</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Sun, 11 Apr 2021 06:35:55 GMT</pubDate><content:encoded><![CDATA[<p>I'm deploying LiveView for the first time and what a strange sight. Browser says <code>Internal server error</code>, yet server log has no error. I deploy again with <code>:debug</code> logger level ( <code>config :logger, level: :debug</code> ).</p><pre><code class="language-Elixir">Started Koodikool Platform.
[info] Running PlatformWeb.Endpoint with cowboy 2.8.0 at :::4010 (http)
[info] Access PlatformWeb.Endpoint at http://app.koodikool.ee
[info] GET /
[debug] Processing with Phoenix.LiveView.Plug.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 500 in 1ms
[debug] Converted error :function_clause to 500 response</code></pre><p>Great.. So what it's telling me is that there's an error with a function so it turned the error into a 500 response. Thank you very much, but <strong>what's the error??</strong></p><p>Elixir is supposed to log all errors by default, but my experience has proven otherwise. It's not too helpful either that people in the Slack respond with "You should see something logged as an error" (actual response, no fault on their end, I'm the one not understanding my system).</p><p>Not knowing what to do I enable <code>debug_errors: true</code> in the Endpoint config. So now the browser points me in the right direction:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://code.krister.ee/content/images/2021/04/image.png" class="kg-image" alt srcset="https://code.krister.ee/content/images/size/w600/2021/04/image.png 600w, https://code.krister.ee/content/images/size/w1000/2021/04/image.png 1000w, https://code.krister.ee/content/images/2021/04/image.png 1221w" sizes="(min-width: 1200px) 1200px"></figure><p>Looking at code it's apparent that the problem is something to do with the server file structure (data directory). So I literally can't test in local. And making a staging server also beats the point since it's not identical with production.</p><p>So what are we supposed to do in this situation? Why is there no error? I'll update if I learn what I should really do.</p>]]></content:encoded></item><item><title><![CDATA[2020 and Onward]]></title><description><![CDATA[<p>The lockdowns didn't really affect me all that much. I was just happy there was a reason for companies (and us) to try out remote working and pollute less.</p><p>The main product I made was Bashboard. It currently has 1 user and he doesn't reply by email. I've thought about</p>]]></description><link>https://code.krister.ee/2020-and-onward/</link><guid isPermaLink="false">60485273c1d3e80f1f871c48</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Mon, 15 Mar 2021 05:28:00 GMT</pubDate><content:encoded><![CDATA[<p>The lockdowns didn't really affect me all that much. I was just happy there was a reason for companies (and us) to try out remote working and pollute less.</p><p>The main product I made was Bashboard. It currently has 1 user and he doesn't reply by email. I've thought about possible pivots, but really I don't think there's a lot to do with it, besides keep it as a portfolio project.</p><p>Overall we've been running our software agency. It's been hard, then fun, then hard again. The best part about it are the people. The worst part that it the work itself doesn't really speak to me.</p><p>January 1st my daughter was born. What a special day that was. What a special few months it's been. I'm happy to be able to just sit there and spend time with her.</p><p>Through some stumbling around I've managed to start a new project, or rather reboot an old love of mine - Koodikool (Code School). It used to be a local group of kids and adults who came together to program something. Now many pieces have come together to perhaps be a startup instead. It feels like backwind.</p><p>Yet today (2021 march) I'm not sure where to go from here. If Koodikool fails then I don't feel like working on any more ideas. I also don't feel like going back and finding a place in the agency (from which I've detached and drifted quite far). We have an awesome client, but I'm not sure my goals align with him or the agency.</p><p>What are my goals? I'd like to see pro's making products. I'd like to make a change in education (hoping for Koodikool). I'd like challenges in service design. Code wise I'd love to write Elixir. And of course on top of that I don't want to give away what I have already: freedom and autonomy.</p><p>Realistically what the agency needs now is systematic company building skills. No matter how much I try - that is not me. I'm more of a product building tornado (tuulispask). The only difference really seems to be that company building is for a few select people and product building is for a larger audience.</p><p>In 5 years this company will be many times larger and more successful. I wouldn't be surprised if <em>then</em> it will also have their own product. But it wont be because I'm a better product developer. No, I'd be the same. The product will come from the outside. Which would be fine if it wasn't a waiting game.</p><p>If I do leave it will likely go on the wall of companies that got successful after I left. I'll take it easy for now. These are just my current feelings. Perhaps Koodikool will turn out to be something.</p><p>(edit: I was already offered a chance to train myself into a product lead, which is an amazing idea and opportunity. Things are looking up all of a sudden)</p>]]></content:encoded></item><item><title><![CDATA[How to drop database between tests in Elixir and Ecto?]]></title><description><![CDATA[<p><em>Note: Make sure you help out the next dev by commenting if you get stuck</em>.</p><p>You want each test to run in a clean database so they don't interfere with each other. So how do you clear the database?</p><p>Best is to keep your tests in a sandbox. A sandbox</p>]]></description><link>https://code.krister.ee/how-to-drop-database-between-tests-in-elixir/</link><guid isPermaLink="false">60097791c1d3e80f1f871ae5</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Thu, 21 Jan 2021 13:25:38 GMT</pubDate><content:encoded><![CDATA[<p><em>Note: Make sure you help out the next dev by commenting if you get stuck</em>.</p><p>You want each test to run in a clean database so they don't interfere with each other. So how do you clear the database?</p><p>Best is to keep your tests in a sandbox. A sandbox is basically a temporary database that gets deleted automatically. Here's how to set it up.</p><h3 id="1-config">1. Config</h3><p>First configure the testing environment to use sandbox in <code>config/test.exs</code>:</p><pre><code class="language-elixir">config :bashboard, Bashboard.Repo,
  # your connection settings
  pool: Ecto.Adapters.SQL.Sandbox</code></pre><h3 id="2-overwrite-the-test-command">2. Overwrite the test command</h3><p>The command <code>mix test</code> runs tests. Since we also want to migrate the database before tests are run, make sure you overwrite the <code>test</code> command in <code>mix.exs</code>:</p><pre><code class="language-elixir">  defp aliases do
    [
      test: ["ecto.create", "ecto.migrate", "test"]
    ]
  end</code></pre><p>That should be there by default, if you used Phoenix to generate your project structure.</p><h3 id="3-start-sandbox">3. Start sandbox</h3><p>The <code>test_helper.ex</code> file is what actually starts the tests. In here we also enable sandbox mode:</p><pre><code class="language-elixir">Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)</code></pre><p><code>:manual</code> here means that the database is only available in the test process. So if inside the test you start another process (with Task for example) then that will fail. This is the preferred way. Only enable <code>:shared</code> more if needed (example later).</p><h3 id="4-define-pre-test-action">4. Define pre-test action</h3><p>You need to checkout a new database connection before each test. This ensures that the database is reduced to the starting state.</p><p>The simplest way is to include a setup function in the beginning of the test module.</p><pre><code class="language-elixir">setup do
  Ecto.Adapters.SQL.Sandbox.checkout(MyRepo)
end</code></pre><p>Usually you have many test files though so rewriting this makes no sense. Instead you can define a function somewhere. Let's write a <code>test/test_conn.exs</code> file:</p><pre><code class="language-elixir">defmodule MyApp.TestConn do

  def checkout(_context \\ nil) do
    Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
  end
  
end</code></pre><p>Now you can just import it in the beginning of any test module and that setup function will run in all the tests in that file:</p><pre><code class="language-elixir">defmodule Bashboard.UserTest do
  use ExUnit.Case
  use Plug.Test
  import MyApp.TestConn

  setup :checkout

  test "description" do
    # your test
  end
end
</code></pre><h1 id="troubleshooting">Troubleshooting</h1><h3 id="1-process-ownership-error">1. process ownership error</h3><pre><code class="language-elixir">** (RuntimeError) cannot find ownership process for #PID&lt;0.35.0&gt;</code></pre><p>This error tells you that you tried to use the DB connection outside of the intended <code>test do</code> process. For example by invoking a Task. This happens because of that <code>:manual</code> setting. The idea is to limit the scope of the sandbox so it's explicit.</p><p>To fix this tell the connection to be <code>:shared</code> instead:</p><pre><code class="language-elixir">defmodule MyApp.TestConn do

  def checkout(_context \\ nil) do
    Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
    Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
  end
  
end</code></pre><p>Now any process that needs the database can access the same one the test process is using. Make sure you always checkout a connection before sharing it though! Sharing a connection means you can't run in concurrent mode though (running multiple tests at the same time).</p><p>To keep tests running concurrently, you can use the more cumbersome, but specific <a href="https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html#module-allowances">allowances</a>.</p><h3 id="2-database-still-not-empty">2. Database still not empty</h3><p>You may have some data in the test database from previous runs. So either just empty it once with <code>mix ecto.drop</code> or add the same command to the alias list:</p><pre><code class="language-elixir">  defp aliases do
    [
      test: ["ecto.drop", "ecto.create", "ecto.migrate", "test"]
    ]
  end</code></pre><p>Generally this should not be needed.</p>]]></content:encoded></item><item><title><![CDATA[Nueheara IQ Buds Max 2 Review]]></title><description><![CDATA[<h3 id="factory-issues">Factory issues</h3><p>The first pair I got the right earbud was dead. Wouldn't even turn on. Support has been good though and 4 months later I got a new pair.</p><p>This time the touch controls only register single taps. Much worse however is that they keep disconnecting every 10 minutes.</p>]]></description><link>https://code.krister.ee/nueheara-iq-buds-max-2-review/</link><guid isPermaLink="false">5fdcc67cc1d3e80f1f871a8f</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Fri, 18 Dec 2020 15:35:39 GMT</pubDate><content:encoded><![CDATA[<h3 id="factory-issues">Factory issues</h3><p>The first pair I got the right earbud was dead. Wouldn't even turn on. Support has been good though and 4 months later I got a new pair.</p><p>This time the touch controls only register single taps. Much worse however is that they keep disconnecting every 10 minutes. This is incredibly annoying and I will be sending these back also. Again, support is a tad slow but still good and I'm sure I'll get a working pair eventually.</p><p>I do believe they are working at their max capacity so I that's all I'll say about these issues for now.</p><h3 id="my-over-sensitive-ears">My over-sensitive ears</h3><p>I have over-sensitive ears. 4 people speaking will make my ears crackle and deaf after a while. Whether I have hyperacusis or misophonia the doctors have not diagnosed, but the problem is real.</p><p>I got the new IQ Buds Max 2 to deal with this. So I can turn down loud sounds and keep conversations.</p><p>Audio tests show that my hearing is perfect. Even the app's Ear ID test says so. The rings are complete, thus I hear all sounds great.</p><figure class="kg-card kg-image-card"><img src="https://code.krister.ee/content/images/2020/12/image.png" class="kg-image" alt srcset="https://code.krister.ee/content/images/size/w600/2020/12/image.png 600w, https://code.krister.ee/content/images/2020/12/image.png 752w" sizes="(min-width: 720px) 720px"></figure><p>What nobody seems to test is how much sound can I handle and what happens then.</p><h3 id="iq-buds-usefulness-to-my-overly-sensitive-ears">IQ Buds usefulness to my overly sensitive ears</h3><p>Not much. The ANC is much less than the physical sealing. The different modes of letting me hear conversations end up greatly over-powering a fan in the corner and leaving me deaf for the person speaking in front of me. When a dog barks.. well let's say I'd rather keep incoming sound off in most occasions.</p><h3 id="overall-quality">Overall quality</h3><p>Sound is about the same as 7€ JBL wired in-ear buds. ANC is around 20% as strong as the physical sealing, so pretty much pointless. Otherwise they stay in the ear very well and I enjoy using them. I just wonder if the same result would have been achieved with 100€ buds instead of 300€.</p><h3 id="summary">Summary</h3><p>My current experience is that they are overpriced and useless. If you have hearing loss I think these can actually benefit you a lot. But for my hearing problems, a solution does not exist currently.</p>]]></content:encoded></item><item><title><![CDATA[Hiring: Culture and results]]></title><description><![CDATA[<p>We just hired our first non-technical person. Her responsibility is sales. Her skill is business analysis. I've known her for a while and did wish there was a way for us to work together. Yet she is a classy and well kept young lady. We're a bunch of dudes making</p>]]></description><link>https://code.krister.ee/hiring-culture-and-results/</link><guid isPermaLink="false">5f8b3c2ff457bb23f900b378</guid><category><![CDATA[startup]]></category><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Sat, 17 Oct 2020 19:28:54 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1510146758428-e5e4b17b8b6a?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1510146758428-e5e4b17b8b6a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Hiring: Culture and results"><p>We just hired our first non-technical person. Her responsibility is sales. Her skill is business analysis. I've known her for a while and did wish there was a way for us to work together. Yet she is a classy and well kept young lady. We're a bunch of dudes making bad jokes and fun of each other. I honestly did not think there was any way she would like our gang. Then I introduced her to one of my partners, they hit it off and the next thing I know she's in the office blending in with the rest of the team as if nothing ever changed. She continues to analyze the team and presents her grand plan of achieving growth which I found absolutely amazing. She fits incredibly well and is determined to reach her targets in sales. I can't even figure out how this all happened and here we are.</p><p>Culture and results. Talent doesn't help the team and hanging out <em>really well</em> does neither. We need to be on common ground, understand eachother and strive for the same goals. What's our recruitment process for this? It's simple.</p><p>A new recruit gets an interview with the founders and any interested party, like the team they will be joining. The interview's only purpose is to get a gut feeling of the person. Dreams, passions, personality. Best recruits just hit it off naturally, but we do give time if we see potential.</p><p>We <em>may</em> read your CV and GitHub to get a feel for technical skills beforehand, but we don't take that too seriously. I seldom open those links actually. We don't give you a fake assignment either. The only way to be sure of your skillset is to get right to work! We pay for a day or a week of work just to see how far you get. I don't care if you graduated top of your class if you can't write a simple function. I also don't care that you're a beginner, if you actually advance the project.</p><p>So we hire fast. We also fire fast. One dude didn't last a day, because he just plain ignored my instructions for the task. We don't do this to be a startup. We do this, because there is no other way to make sure we match.</p><p>We also fired a partner (two, actually). Because while a great fit culture wise, the results were either small or just in a different direction. I'm not proud of any firings. But I'm glad to see the company continue in a mutual understanding.</p>]]></content:encoded></item><item><title><![CDATA[My First Newsletter (remember me?)]]></title><description><![CDATA[<p>Hey. You subscribed to my blog either a few months ago or perhaps many years ago. I haven't sent out a newsletter ever. So hello again!</p><p>I write irregularly. Some months there's many posts every week. Some months not a single one. That's not going to change. No promises. But</p>]]></description><link>https://code.krister.ee/my-first-newsletter-remember-me/</link><guid isPermaLink="false">5f85f1eff457bb23f900b329</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Tue, 13 Oct 2020 18:31:14 GMT</pubDate><content:encoded><![CDATA[<p>Hey. You subscribed to my blog either a few months ago or perhaps many years ago. I haven't sent out a newsletter ever. So hello again!</p><p>I write irregularly. Some months there's many posts every week. Some months not a single one. That's not going to change. No promises. But I'm going to focus on making posts that share my learning's in leading a company.</p><p>Me and two other founders (and some more awesome people) run a software development company. I'm otherwise a startup kind of guy, but this is the only thing that's stayed in business for 2 years now. I learn a lot every day and I want to share it.</p><p>So topics are going to be: business, programming, schools (I like teaching). Probably some random personal stuff every now and again. My most popular post is a technical configuration tutorial, yet my most emailed about post is orbits stress and depression. People have found a variety of topics helpful.</p><p>I'm going to email these posts out for a while just to see what happens. What feedback I get. Feel free to unsubscribe. Just like I do most newsletters. I don't have an aim to grow this audience as much as let people find me, who actually benefit from this. I'd like to hone my writing skills.</p><p>So either enjoy or just see you later.</p>]]></content:encoded></item><item><title><![CDATA[A team is worth more than the sum of people]]></title><description><![CDATA[<p>Me and my partners agree that me going off and doing some startup team is not a recipe for success. One person just can't think of everything and one just needs to bounce off ideas and think of more options than the one brain can handle.</p><p>A less apparent surprise</p>]]></description><link>https://code.krister.ee/a-team-is-worth-more-than-the-sum-of-people/</link><guid isPermaLink="false">5f85d3e1b304af6707bd4007</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Tue, 13 Oct 2020 16:26:03 GMT</pubDate><content:encoded><![CDATA[<p>Me and my partners agree that me going off and doing some startup team is not a recipe for success. One person just can't think of everything and one just needs to bounce off ideas and think of more options than the one brain can handle.</p><p>A less apparent surprise was that even when the project is super small, having extra two people to work with boosts work morale through the roof! It's a whole other experience. Motivation loss is barely a thing. Now it really is a matter of figuring out a work routine.</p><p>I'm going on child leave for 3 months soon. I hope the two can stay motivated together. We've experienced the single developer burnout multiple times already.</p><p>In order to keep these two happy I'm involving self-development time regularly. If the project is small and the direction is dependent on the clients feelings, motivation will naturally disappear at one point. So I'm doing my best to take care of the employees.</p><p>In any case. Having a team is one of those obvious things that took years to find out. I'm happy I'm here now.</p>]]></content:encoded></item><item><title><![CDATA[Google Analytics lies.]]></title><description><![CDATA[<p>We all know that people use ad blockers and thus you probably have more visitors than the numbers show (solution: custom fetch request or server side analytics). Well today I found out Google Analytics with all it's amazing power isn't doing much to mitigate bots.</p><p>Check this out. On the</p>]]></description><link>https://code.krister.ee/google-analytics-lies/</link><guid isPermaLink="false">5ec23c05b304af6707bd3fb0</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Mon, 18 May 2020 10:13:39 GMT</pubDate><content:encoded><![CDATA[<p>We all know that people use ad blockers and thus you probably have more visitors than the numbers show (solution: custom fetch request or server side analytics). Well today I found out Google Analytics with all it's amazing power isn't doing much to mitigate bots.</p><p>Check this out. On the 15th of May I was supposed to have 38 users.</p><figure class="kg-card kg-image-card"><img src="https://code.krister.ee/content/images/2020/05/image-4.png" class="kg-image"></figure><p>I had put a custom fetch request into that page and sent the data to Bashboard.</p><pre><code class="language-JavaScript">    const previousVisit = localStorage.getItem('log-0-blog')
    if (!previousVisit) {
        localStorage.setItem('log-0-blog', true)
        fetch('https://bashboard.io/cpconf/users/funnel', {
            method: 'POST',
            body: JSON.stringify('0-blog')
        })
    }</code></pre><p>15th of May actually had 20 visitors!</p><figure class="kg-card kg-image-card"><img src="https://code.krister.ee/content/images/2020/05/image-3.png" class="kg-image"></figure><p>So either my localStorage trick is more faitful than Google Analytics <a href="https://www.google-analytics.com/analytics.js">megascript</a>, or the problem is a lot of bots don't run fetch() commands. They do however run JS otherwise Google wouldn't see the visitor either.</p><p>If you know what's up with this - let me know in the comments (or <a href="https://www.indiehackers.com/post/google-analytics-lies-9f809fe865">IH comments</a>).</p>]]></content:encoded></item><item><title><![CDATA[Bashboard flunked the PH launch]]></title><description><![CDATA[<figure class="kg-card kg-embed-card"><iframe style="border: none;" src="https://cards.producthunt.com/cards/posts/198222?v=1" width="500" height="405" frameborder="0" scrolling="no" allowfullscreen></iframe></figure><p>Which is not a suprise. I was however pleasently surprised how many people voted for me when posting to my social groups. I mean 15 isn't much, but it's a lot more than what I was expecting :)</p><p>So anyway what are <a href="https://bashboard.io/bashboard/landing">the stats</a>?</p><figure class="kg-card kg-image-card"><img src="https://code.krister.ee/content/images/2020/05/image.png" class="kg-image"></figure><p>So on PH launch day there were</p>]]></description><link>https://code.krister.ee/bashboard-flunked-the-ph-launch/</link><guid isPermaLink="false">5ebaa6acb304af6707bd3f31</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Tue, 12 May 2020 14:02:13 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-embed-card"><iframe style="border: none;" src="https://cards.producthunt.com/cards/posts/198222?v=1" width="500" height="405" frameborder="0" scrolling="no" allowfullscreen></iframe></figure><p>Which is not a suprise. I was however pleasently surprised how many people voted for me when posting to my social groups. I mean 15 isn't much, but it's a lot more than what I was expecting :)</p><p>So anyway what are <a href="https://bashboard.io/bashboard/landing">the stats</a>?</p><figure class="kg-card kg-image-card"><img src="https://code.krister.ee/content/images/2020/05/image.png" class="kg-image"></figure><p>So on PH launch day there were 329 visitors. Next day a bit less. Those days actually include the HN launch I did in the same evening since the DB was beefed up anyway and no tmuch traffic was happening. HN resulted in 0.</p><p>The most popular page besides landing was /price and then /examples. The least popular were /roadmap, /usecases and /about. I probably spent too much time on those.</p><p>The stats show 18 registered users, but 7 of those are either spam or myself. So 11. Not the worst, but <strong>there are no active users since launch</strong> - and that's the important bit.</p><h2 id="what-s-next">What's next?</h2><p>Marketing. I'll go into feature avoiding mode and only implement something if:</p><ol><li>An active user asks for it.</li><li>I need it to see how the marketing is doing.</li></ol><h2 id="le-grande-marketing-plan">Le Grande Marketing Plan</h2><p>I'm a noob. But I have some ideas.</p><ol><li>Hang out in communities and build a reputation and suggest Bashboard when actually appropriate.</li><li>Keep myself up to date with competitors blogs to learn about my own customer.</li><li>Find individual users who may be interested by utilizing a few specific growth hacks.</li><li>Blog about trendy topics like how to get live charts into Notion :)</li><li>Make public charts that are useful/interesting and spread those.</li></ol><p>That's it! Yeah it's not much I know. But it's a marathon, not a sprint. New ideas will arise, old ones will fall. A few actions will prove more beneficial than others. Just keep moving.</p><h2 id="but-first-">But first!</h2><p>During the three month bashing out of Bashboard - I learned some things. And had a few ideas. I want to try them out. It'll only take a few weeks, I promise! :D</p><p>But actually yes, this blog has some attention from the internet, I want to try and use it. Also Sales Safari has been a mind blower. Definitely want to go in and find some watering holes of the internet that I could perhaps serve.</p>]]></content:encoded></item><item><title><![CDATA[Server Sent Events with Elixir]]></title><description><![CDATA[<p>SSE is old, but not a lot of Elixir materials out there. Took me 3 days to get this working, so congrats for finding this :) This is not a tutorial, but a code example from my project (it's open source so you can <a href="https://gitlab.com/bashboard/bashboard-backend/-/blob/master/lib/router/widget_router.ex#L95">see the actual code</a>).</p><p>This post will</p>]]></description><link>https://code.krister.ee/server-sent-events-with-elixir/</link><guid isPermaLink="false">5ea00b55b304af6707bd3ea2</guid><category><![CDATA[elixir]]></category><category><![CDATA[TIL]]></category><category><![CDATA[tutorial]]></category><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Wed, 22 Apr 2020 09:37:28 GMT</pubDate><media:content url="https://code.krister.ee/content/images/2020/04/screenshot-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://code.krister.ee/content/images/2020/04/screenshot-1.png" alt="Server Sent Events with Elixir"><p>SSE is old, but not a lot of Elixir materials out there. Took me 3 days to get this working, so congrats for finding this :) This is not a tutorial, but a code example from my project (it's open source so you can <a href="https://gitlab.com/bashboard/bashboard-backend/-/blob/master/lib/router/widget_router.ex#L95">see the actual code</a>).</p><p>This post will show how to:</p><ol><li>Set up Elixir to send SSE events</li><li>Set up Pubsub for Elixir, to trigger an SSE event</li><li>Set up a Javascript frontend to receive</li></ol><h2 id="elixir-side-parts-1-and-2-">Elixir side (parts 1 and 2)</h2><p>First we have to install <code>pubsub</code> so change these files:</p><pre><code class="language-Elixir"># mix.exs
defp deps do
[
{:pubsub, "~&gt; 1.0"}  # Add this line
]
end</code></pre><pre><code class="language-Elixir"># application.exs

  def start(_type, _args) do
    children = [
      Plug.Cowboy.child_spec(
        protocol_options: [idle_timeout: :infinity] # This is needed
      ),
      {PubSub, []} # Add this
    ]

    opts = [strategy: :one_for_one, name: Bashboard.Supervisor]
    Supervisor.start_link(children, opts)
  end</code></pre><p><code>idle_timeout: :infinity</code> is needed for cowboy (also if you use Phoenix), as otherwise your connection will time out in 60 sec with a confusing error:</p><pre><code class="language-Elixir"># Firefox
The connection to URL was interrupted while the page was loading.

# Chrome
net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)</code></pre><p>So here's the router code (using Plug only) with my specific parts removed:</p><pre><code class="language-Elixir">
  get "/sse" do
    conn =
      conn
      |&gt; put_resp_header("Cache-Control", "no-cache")
      |&gt; put_resp_header("connection", "keep-alive")
      |&gt; put_resp_header("Content-Type", "text/event-stream; charset=utf-8")
      |&gt; send_chunked(200)

	# Listen to events in the topic :cagg
    PubSub.subscribe(self(), :cagg)

    # This loop will wait for messages infinitely
    sse_loop(conn, self())
  end

  defp sse_loop(conn, pid) do
    # receive is what stops the router from processing
    # and waits for an event to come in
    receive do
      # Send updates when :cagg is finished.
      {:cagg, :done} -&gt;
        # Query for updates.
        widget = get_new_data()

        # Send update.
        chunk(conn, "event: message\ndata: #{Poison.encode!(widget)}\n\n")
        
        # Wait for next publish.
        sse_loop(conn, pid)

      # Stop SSE if this conn is actually down.
      # Ignore other processes finishing.
      # Notice the "^" in front of pid.
      {:DOWN, _reference, :process, ^pid, _type} -&gt;
        nil

      # Don't stop SSE because of unrelated events.
      _other -&gt;
        sse_loop(conn, pid)
    end
  end</code></pre><p>That's the Elixir part. Now if I call <code>PubSub.publish(:cagg, {:cagg, :done})</code> anywhere in the app, SSE will fethc updates and send to client.</p><h2 id="javascript-client-part-3-">Javascript client (part 3)</h2><pre><code class="language-Javascript">const source = new EventSource(sseURL);

source.addEventListener("error", function(event) {
	console.error("[SSE close]", event);
});

source.addEventListener("open", function(event) {
	console.info("[SSE open]", event);
});

source.addEventListener("message", function(event) {
	// update your data or whatever
});</code></pre><hr><p></p><p>So it's not so hard at all, right? Well I saved you 3 days so high five!</p>]]></content:encoded></item><item><title><![CDATA[onMount doesn't fire Sapper/Svelte]]></title><description><![CDATA[<p>I only have two pages A links to B. Both have a preload of data. So I load A, everything is nice. Click on the link to B and voila - the new layout and everything, but the onMount from A renders components into page B. WTF? What happened?</p><p>Turns</p>]]></description><link>https://code.krister.ee/onmount-doesnt-fire-sapper-svelte/</link><guid isPermaLink="false">5e8f11d5b304af6707bd3e7b</guid><dc:creator><![CDATA[Krister Viirsaar]]></dc:creator><pubDate>Thu, 09 Apr 2020 12:19:27 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1503525148566-ef5c2b9c93bd?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1503525148566-ef5c2b9c93bd?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="onMount doesn't fire Sapper/Svelte"><p>I only have two pages A links to B. Both have a preload of data. So I load A, everything is nice. Click on the link to B and voila - the new layout and everything, but the onMount from A renders components into page B. WTF? What happened?</p><p>Turns out I had a small error in onMount A, which caused the app to get confused. I don't know why or how. Just make sure you handle all errors on onMount.</p><p>Took me 4 hours today. Since problem was only in production and I couldn't reproduce it. Eff..</p>]]></content:encoded></item></channel></rss>