In the last week I was a part of a few extra meetings alongside the regular team stand ups for our open source project Starchart, a meeting at the start of the week to discuss the new SLO implementation with David where we arrived at a neater solution for the SLO cookie and made it strict and restricted it to the logout route. At the same time we made the session cookie strict, I had forgotten what I had seen when looking at telescope's cookies. We also made some UI changes to the logout prompt making a less prominent button that would initiate SLO and a more vibrant red one that would just redirect you to sign in having only invalidated the session cookie with the starchart website. In any case, I was able to land the new SLO flow at the beginning of this week after some discussion and then demonstrated in a consequent mid-week meeting with the team PR #366.
I also did a few reviews, although here I will only mention one of them since I felt I added a bit more to this PR than others PR #414.
Cookie problems
Here we have the impromptu bug that refreshed my memory on one of the problems with cookies when doing SSO. When my SLO change was incorporated into staging it seemingly broke something when signing in, making it so that a user would have to sign in twice to be authenticated to Starchart. My first instinct was to check the networking tab and to take a look at the cookies because I remembered we set the session cookie to strict and lo and behold the session cookie was being rejected since the browser claimed it set as a result of a cross-site request. The browser was of course correct, the session cookie is set on our login callback as a result of a request from our IDP which is a different origin and indeed cross-site, since we set the session cookie to be strict it would not be set by any cross-site requests at all. The simple solution here was to set the cookie to lax instead of strict and allow it to be set as a result of a cross-site request and this seems to be the general consensus when it comes to SSO seeing as session cookies are very often set as a result of a callback from an entity separate from our website. We did however keep the SLO cookie as strict since it really only exists and needs to be used within the one logout route that I have written within our own application. In any case here is the one word PR that fixed our double login issue PR #427
Next Steps
Next I need to take a look at allowing admins to impersonate users in Starchart which I should do by adding information to the session, an "effective user" field that will come empty for admins so that they can set it to who they would want to impersonate and that would come forcibly filled with a regular user's own username so they can not in fact impersonate anyone else.
Hello, my friends! I'm delighted to see you again. This week, I tackled two significant PRs: one focused on DNS records validation, and the other on admin page layouts.
DNS Record Validation
To be honest, I found this task more engaging than the other one, as it introduced me to in-depth validation using zod, unlike designing layouts. The objective was to perform comprehensive validation of all required fields with zod, return appropriate messages, and then parse the response on the frontend to display them to the user.
One interesting technique I employed was reusing a single zod schema for both record creation and record editing requests. To make this work, I only needed to extend the base record validation schema with an additional field. This process closely resembles inheritance in interfaces or classes, and it always feels rewarding to reuse functionality. After some follow-ups, the PR was merged.
This task was more straightforward: our goal was to create an admin page, and the initial step from a frontend perspective was to design layouts for it. I crafted two components: one for the metric card and another for the users table. The result looks quite polished, but the real fun will begin when we delve into server-side development.
Next week, I'll be taking on the role of a sheriff, which comes with its own set of responsibilities. Although I can't say I'm overly enthusiastic, it will be interesting to see how the experience unfolds. I'll likely collaborate with Stephan on admin backend development since he's leading that effort. Additionally, I might tackle some smaller issues.
Final Thoughts
My ongoing battle against social media distractions continues. While I'm doing alright, there's room for improvement. I also feel that I could have been more efficient with my work on the StarChart this week and delivered results faster. I'll need to reflect on that. Peace! ✌️
This past week, I worked on a bunch of small issues for starchart to improve our UI experience, fix some bugs, and update test configuration:
We needed some new scripts to setup a test database to use when running automated tests. This basically involved copying the existing setup script and changing the database url (PR)
Some updates were needed for our vitest configuration. These were mostly small changes, and also another script to add a coverage report for unit tests. This also raised some discussion on adding coverage for out end to end tests. (PR)
A UI change to hide the DNS Records table if none exist for the logged in user. Instead, a create button is displayed in the center. (PR)
Another UI for the DNS Records table to disable the renew record button if the DNS record expiry date 6 months from now or beyond. (PR)
A change to improve the accessibility of some links on the landing page. This was needed as the light blue color didn't provide enough contrast. (PR)
Another issue that I have been working on but was not able to finish the last week is to handle the fact that users can be deactivated. This involves checking for a user being deactivated in multiple places throughout the code as a user could be deactivated while a some jobs are running to update DNS Records, etc.
Roadblocks are part of the journey toward finding your footing in the programming industry. It's on us how we maneuver our way around these obstacles to get to where we want in our careers.
In this blog post, I will review five major roadblocks new programmers will likely experience.
1. Not knowing where to start
Not knowing where to start can be frustrating for all of us, especially when employing a technology stack we are unfamiliar with or uncomfortable with. To overcome this challenge, begin with what you know and gradually expand your understanding.
2. Focusing on the problem rather than the solution
We often make the problem tenfold worse by focusing on the problem rather than the solution. For instance, while skiing, if we focus only on the trees rather than the path we should follow, we will only see trees.
Invest your energy in the desired outcome rather than the problem. A Solution-Focused approach to solving problems can help you get results faster.
3. Being afraid of asking for help
Sometimes the bravest thing we could do is ask for help. Of course, we should first try to figure out the problem independently, but if we have to meet specific deadlines and are still stuck, we should feel good about asking for help from community members.
4. Comparing yourself to others
We should never compare ourselves to others, no matter what. It does more harm than good to one's self-esteem. We should always remind ourselves that everyone starts the same journey, with the same struggles we currently face, to get where they are today.
5. Giving up
The thought of giving up should cease to exist in our minds because it's okay to struggle and be bad at something to get better at it. Replace such thoughts with positive affirmations to keep you going. One must Keep failing until one succeeds, without quitting. Remember, making something extraordinary requires struggle, patience, and dedication.
If this post was helpful, drop a like. If I missed an important point, leave it in a comment, as it would help me and others get better at programming one day at a time. :)
The progress this week hasn't been ideal when it comes to contributing to our open source project starchart. Learning how the back-end function works took an entire's day of work. Partner it up with my other project for my other course, and it just feels overwhelming. Of course, the fault can be mainly attributed to my irresponsibility of handling the time I spend with individual courses.
Learning how the function works
Looking through the code presented by the certificate queue function, it can be a bit confusing, when looking through it the first time. When first looking through the code, sweat and a sense of panic engulfs me as I usually do. When it was me at the start of the course, I would just suck this up, and try to learn everything by myself, which can lead to more confusion, and time wasted. However, that wasn't the case this time around. I asked my peers on how things works, which increased the speed of my progress.
ChatGPT
One thing I did that other people may considered as "taboo", is the used of ChatGPT. I would never suggest this to anyone who just wants an easy answer. The benefits of AI for us developers is not that it can write us what we want (at least for now...), but that it provides us with information that can help us better our knowledge. I will provide you with an example from how I tackled my issue.
When starting my issue I tend to look at what other contributors has done in the area I will be working on. In this case I'm looking at the UI. Thankfully enough, a fellow contributor had already merged a PR with DNS-Records. I messaged them, and asked how they did their code, but unfortunately they didn't respond(Not their fault, it is a busy semester), then I went to ask another who developed the function that I will be using. Unfortunately, they have a class that day, and couldn't answer my question at the moment-- another few hours spent figuring things out
Do you notice a pattern? People are busy, and there's nothing you can do. You can figure things out yourself, but more than likely you would not find an answer. I do recall being mentioned from the previous meeting about a file that does the function step by step that being dev.tsx. There I noticed another similar pattern from the DNS-Record, and that is the "action". Thus I asked ChatGPT...
What condition can cause "action" to run?
Of which it provided me with the information that I wanted
That is when everything clicked to me. I finally understood how he handles the connection, and I managed to finally start making progress. By the end of the day, I finally managed to create a certificate, and although it wasn't perfect it did provide me with a sense of ease that I may actually complete this by the end of this milestone. I forgot to mention that after knowing this, I figured out that action is actually a remix route module, which boosted my understanding of it.
Conclusion
This week has been quite stressful, but thankfully it made me realize the importance of information. ChatGPT speeds up our ability to learn things, by providing us fast and accurate answers. It may not have given me the answer I "wanted", but it did provide me with information that I "needed".
ChatGPT continues to rule the news cycle. It's also increasingly finding its way into my work on our department's Academic Integrity Committee, where we've seen it pop up more and more in reported cases: students using it on tests, both paper-based (cell phone pic of a question uploaded to a WhatsApp group, where a friend types it into ChatGPT) and electronic, as well as in assignments. Much of the academic dialog online and at our institution has been focused on ChatGPT as a vector for cheating.
Meanwhile, a decade's worth of innovation has happened in the past few weeks as an arms-race plays out for control of AI in big tech. I was trying to keep track of everything I saw being released, but it's impossible. The rate of change is beyond anything I've ever experienced. The idea that an instructor or institution can write a policy that gets on top of this is laughable.
Despite my concerns over academic integrity, I'm more interested in understanding how to properly use AI in teaching, learning, and development. As a result, I've been trying to take inspiration from people like Simon Willison, who is using ChatGPT and Copilot to learn new technologies (e.g. Rust, AppleScript) and taking notes as he goes.
So this past week I challenged myself to try and use ChatGPT in order to better understand how my students are encountering it, and what lessons I could learn in order to teach them how to make better use of the responses they are getting.
I started the challenge by building ChatGPT in CSS for my second-semester web programming class. We were studying CSS Layout, and the ChatGPT site is a fairly simple one to create, using the topics for the week:
ChatCSS - recreating ChatGPT in CSS for my Web Programming class
Writing the code, I spent a long time using their interface. I was struck by the Limitations column, which I doubt most people have read:
May occasionally generate incorrect information
May occasionally produce harmful instructions or biased content
These are incredible warnings to see displayed on a web site. "This site might harm you." Wow! We could argue that this warning should be placed on lots of the web, but to see it called out like this is fascinating and not something I encounter very often. I thought about that a lot this week.
I went to class on Monday, plugged my laptop in, and started with this ChatCSS tab open. "Does anyone recognize this site?" Silence! Eyes down. Nervous laughter. It was like I had just begun a lecture about sex. It's clear that ChatGPT has quickly earned taboo status in academic discourse between professors and students. That's too bad, because I believe it needs to get discussed openly.
Spending two lectures this week building the ChatCSS UI and talking about how it works allowed me to start to engage with the students, hear their ideas, discuss their questions. They are absolutely using it, but mostly having to hide that fact. They need help navigating this new world. From this experience I realized that I need to talk about it more, not less.
Next, I did a bunch of reviews for my open source students working on the Starchart DNS/SSL project. A number of times as I read their code, I asked questions about what I was seeing, and the answer came back, "ChatGPT recommended this." I've never encountered this before. ChatGPT was a third entity in our conversations, always there but also completely absent. For example, it wanted to use a 403 HTTP status code to indicate that a resource being created asynchronously isn't ready yet (I'd use a 409). Or writing regular expressions to cover validation cases on IPv4, IPv6, and domain names. I wanted to know about edge cases that were missed. Having ChatGPT's output, but not ChatGPT itself, made the process a bit painful. The code raised questions that couldn't be answered, which felt unsatisfying. I learned that detaching text from a ChatGPT response, but still insisting that "ChatGPT wrote it" won't work. Once you pull that text out of the ChatGPT window, you have to own it, answer for it, clean up after it.
I had a number of times this week were I struggled to name something in code, and I tried using ChatGPT to help me explore possible options. For example, we have been dealing with overlaps between name vs. subdomain vs. fqdn in our Starchart code. I didn't end up using a lot of what it suggested, but I think the exercise of forcing myself to get it out of my head, and into written form, helped me come to a decision faster. In this way, using ChatGPT was a forcing function. Much as writing an email to a colleague, or discussing with peers on Slack, writing my problem out kickstarted the problem-solving process.
Later in the week I used ChatGPT to solve a real problem I had. I was struggling to write some GitHub Actions code for a Docker build and webhook deployment flow. GitHub Actions are annoying to test, because you have to do it for real vs. running things locally. I couldn't figure out or remember how to do the following:
Interpolate environment variables into JSON strings
Get a short git sha (I only wanted the first part of the string)
I found ChatGPT was eventually able to get me where I needed to be, but it took half-a-dozen questions/responses to get there. For example, it told me to use the substring function:
However, there is no such function (GitHub, please add it!), so I had to press it:
It did help, and I was able to write my workflow YAML file and get automatic deploys to work.
Next, I struggled to make sense of the BullMQ API for dealing with errors in flow jobs. We have a series of asynchronous worker jobs that run in the background, and we need to chain them. When one of them fails, we need to execute an error handler, and the way we were doing it was not working.
I was pleased to discover that simply mentioning the name of the library, BullMQ, was enough and it knew what I was talking about. However, once again, I had to be patient to arrive at my desired destination. I went back and forth with it many times before I could get it to stop hallucinating imaginary APIs and had to explicitly call out its inconsistency:
Not only is ChatGPT going to give you incorrect info, but it also has no problem contradicting itself in a single response. This is something I'm not used to. If a person did this to me, I'd get annoyed; in fact, I was annoyed in the exchange above. Humans won't tolerate being lied to or manipulated like this. This erosion of trust breaks down communication: "How can I trust you if you just lied to me?" The truth is, you can't trust ChatGPT. It neither lies nor tells the truth, flitting effortless between the two, giving what Neil Gaiman called "information-shaped sentences" on Twitter this week.
You have to approach ChatGPT with zero-trust and plan to verify everything. Knowing this, I can route around incorrect information, and try to aim deeper at the truth I really need. This tells me that using responses in areas I know nothing about is going to leave me vulnerable to misinformation. My students, who don't know enough yet to push back at the AI, will struggle when it gives reasonable but false code.
Later in the week, I asked it to rewrite a TypeScript file I was reviewing. I wanted to see what other styles it could imagine for the code, which was somewhat complex to read. It did a nice job of renaming things, switching loop types to make it more readable, using early returns (amen!), etc. It also broke the logic of the code. When I shared it with the original author, he wasn't impressed. Where I found the style vs. the substance of the code interesting, he was 100% focused on the substance, which had problems. This taught me that I need to be clear about my expectations for the use of the text I share from ChatGPT: am I looking for truth or form or both? What does my audience need and expect?
Next, I had some free time and wanted to figure out how to get Microsoft's Megadetector model running with node.js and TensorFlow. I'm really interested in Camera Traps and biodiversity tracking using technology. I spent a long time going back and forth with ChatGPT on this one, before eventually giving up. First, it gave me code to use it remotely via Azure vs. locally (not a bad idea, actually). Once I told it that I wanted to run the model locally, it got me 80% of the way there. However, it gave me invalid model URLs, combined Python/pip and node.js/npm dependencies, and made-up an npm module for the image processing. Once again, I had to be willing to sort through the response and push back in order to have it correct its mistakes.
This can feel uncomfortable, too, since a human at the other end of my needy and critical responses would quickly tire of our conversation. ChatGPT doesn't, and will happily try again. Eventually it became clear that the 2021 model cut-off date wasn't going to work with the current state of the Megadetector repo, and I ran out of time. However, if I'd been willing to keep going a bit longer, I could have got something working.
Next, I decided to port some Python code I'd read online to Zig. I don't know why I've been interested in Zig lately, but I keep coming back to it (I think where Rust feels too much for me, Zig somehow feels just right). I was pleasantly surprised to see how easily this process went. Once again, it hallucinated a mix of pip modules and zig standard library code; however, by now I was used to sorting out the 20% of the response that would need to be thrown away. I liken the process to working with a senior dev who only has 5 minutes to answer your question: the response they give is "right" but includes a mix of real and pseudo-code that you have to expand and debug yourself. Knowing how to set my expectations (that I'll almost never be able to use code it returns as-is, that responses won't be idiomatic to the ecosystem I'm targeting and will often mix ideas from different languages) has made the process less frustrating for me.
Next I tried something completely different and more risky. Having read this thread about a guy who used ChatGPT to help save his dog's life, I tried doing some medical research. Given everything I've said above about needing to be able to verify all responses, not being able to trust at least 20% of what comes back, etc. you might think this is insane. Again, expectation setting is key here. I can't use what it gives back as-is, but by now I don't plan to ever do that. Here, I'm interested in having it guide and inform my research.
I asked about a condition I wanted to know about, starting by asking what the usual way of treating it would be (i.e., without me saying anything). I wanted to see if it could give me information I already know to be true, before going into areas I'm less sure about. It was able to confirm current treatment options, which I know are correct. I then asked for the state of the research into newer methods: what's emerging? It gave me 3 amazing jumping off points, which led me on a fruitful literature review and opened my eyes to some interesting possibilities I hadn't heard about. Here I needed to pair ChatGPT with research into the scientific literature. Unlike code, where I can lean on a mix of my own understanding and tools to verify what I'm being told, I have no ability to assess the value of medical information. One needs to be ready to jump out of ChatGPT and into proper research, but doing so can be a really valuable exercise. I know from experience that I could never get Google to give me this info (I've tried)--I don't know how to put it into a searchable question format. However, after working with ChatGPT, Google once again becomes useful, since I have keywords I can use for search.
Finally, I used ChatGPT to help me quickly do a task that would have been time-consuming to code or do manually. I had a bunch of CSS, and I wanted a sorted list of all the unique CSS properties I'd used. I pasted the entire file into ChatGPT and told it to give me exactly that, which it did. This was probably the most satisfying use I'd had all week. It was something I knew how to do manually, and could also write a program to do, but I wanted it done fast without doing either of those. It "just worked" and got it 100% correct. It made me realize that this kind of language/text programming problem is something I should be feeding to ChatGPT more often. I do a lot of it while teaching, coding, and in my daily life.
This week I found that I had to force myself to use ChatGPT, it wasn't my default. I found the realities of using it both more and less magical than expected. As predicted, I can't trust it. However, if I enter the conversation somewhat adversarially, I can often extract something useful. I'm not talking to AI. We're not chatting. It's much more combative, defensive, and frustrating than talking to a person. I have to verify everything I'm given, which means I have to be in the mood to do that. I can't be tired and let the response wash over me. But I also recognize the value of what I was able to do with it this week. I solved real problems, from technical to academic to personal. It's interesting and I need to try it more.
Today we're shipping Starchart v0.7. This felt like a big one, so I wanted to discuss what we did. I'd say the theme of this release is "2 steps forward, 1 step backward." I'll discuss some of the highlights.
CI/CD and Staging
First, we now have a working CI/CD pipeline to staging! This has taken much longer than I anticipated, and required all sorts of people to lend a hand (thank you to many members of the team who helped me, to Mehrdad in ITS, Chris Tyler, and even ChatGPT!). Our GitHub Actions Workflow runs all of our tests and static analysis tools, then builds and pushes a Docker image to the GitHub Container Registry. We then use a webhook from GitHub Actions to staging to trigger Docker to update the running Docker swarm to the new image tag.
ITS asked us to see if we could avoid sending secrets over the network, which we're doing via an HMAC SHA-256 digest on the webhook body. We have the same encrypted secret on GitHub Actions as well as the staging server, and use it to calculate our digest, which has to match for the webhook to get processed. It works really smoothly.
A major benefit of having a live CI/CD pipeline like this is that you can more easily test features as they land. But testing leads to finding bugs, and this week we found lots of bugs! For example, we learned that Let's Encrypt won't support _s in domain names (our AWS Route53 Hosted Zone at _stage_ in it). We also learned that our SMTP setup for email notifications, which works great in development, isn't working against Office365.
Reconciling our Tech Debt
Another thing that testing does is to reveal inadequate architectural choices. Imagine building a bridge and working from both ends simultaneously toward the middle. Eventually, a small error on one side is going to reveal itself in the middle when things don't line up the way they should.
We experienced this when we landed our Let's Encrypt certificate order code this week and tried to use it for the first time on staging. Denes and I were excited to witness this small victory live, but the server logs told us a different story: our DNS data model and internal API won't work the way we wanted. This realization has led us to move to a new approach, which Denes spent the weekend hacking together.
Our DNS works across two systems: 1) we store DNs record data in MySQL; 2) we deploy those records to AWS Route53. Previously, we did atomic DNS changes. If someone wanted to add a DNS record, we issued a request, waited for Route53 to sync the change, then updated our MySQL database. However, we're going to move to a kind of "Virtual DOM" approach. In much the same way that React deals with state and applies changes to a virtual DOM before diff'ing the real DOM, we're going to maintain our DNS "state" in MySQL and reconcile that with Route53. We take the state of the database and our hosted zone's records and do a comparison: if a record exists in Route53 that's not in MySQL, it needs to get deleted; similarly, if a record exists in MySQL but not in Route53, it needs to get created or updated to match.
Using this new DNS "reconciler," we'll continually sync the state of our database with Route53 in the background, allowing us to simplify the app in the foreground, where we can work exclusively with the database as our "source of truth" and ignore Route53 completely--eventually Route53 will match our database. It will also allow us to quickly reflect the database into Route53 when we make a move (e.g., like when we moved to a new hosted zone this week to get rid of the _stage_ name).
Making this overhaul at such a late stage of development is a concern. Do we have enough time to properly shift all of our code over to the new architecture, fix the bugs it causes, and also complete the other existing work we have? It's a great question! I'm fairly confident that we can, but it's going to require a bunch of co-ordination across the team to sort out all the various systems that need updating. Changes of this scale aren't without risk and cost.
Another side-effect of doing this change is that it's going to trigger us to delete a huge amount of code and tests that Won and others worked hard to write over the past 2 months. I know from experience that it can feel pretty demoralizing to watch your hard work get flushed down the drain. "What was the point of all that?" And yet, without that amazing code having been written in the first place, there's no way that we could have come to understand what your design should actually look like. Brook's says it better than I can:
“Where a new system concept or new technology is used, one has to build a system to throw away, for even the best planning is not so omniscient as to get it right the first time. Hence plan to throw one away; you will, anyhow.” --Fred Brooks, "The Mythical Man-Month"
Like all project leaders, I wish I'd figured this out sooner. What if I'd insisted on more integration testing sooner in the process? Would we be at this stage now, or would it have dragged the inevitable out longer? The truth is, this always happens. Real software projects are full of this stuff and above all, I'm trying to give the students an authentic experience of real-world, open source software development. I'd say this is "mission accomplished!"
Other Notable Changes
I took a stroll through the changesets from 0.6.0 til today, and it's packed with amazing work by the team. Here's some of what I saw:
Won's added code to limit the number of records a user can create, and fixed a bug where we counted the Let's Encrypt TXT records in that value by mistake.
Tymur fixed the UI buttons for the DNS Records to work properly when they shouldn't be usable and added validation to our create/edit form, which is amazing.
Eakam chipped away at many of our front-end "papercuts:" fixing the styling on the Renew button in the DNS Records table, improved the front-page's accessibility, creating a first-run experience when users create their first DNS Record. We also got new icons and a favicon added (goodbye Remix logo!) and fixed some HTML issues that Lighthouse exposed.
TD wrote an epic PR to rename DNS Domain to DNS Record across the entire tree, all APIs, data model, etc to standardize our naming. Won did something similar with the terms Subdomain and FQDN (fully-qualified domain name). We've needed both of these for a while. He also got our notifications wired up to the DNS worker, which I discussed above.
Stefan got our SLO flow finalized and fixed a bad bug we had with overly strict cookie settings. The SAML stuff on staging has been working amazingly well!
I rewrote our DNS worker flow so it properly dealt with error cleanup, and we learned a lot about BullMQ's flows and error patterns this week.
Denes finished the Let's Encrypt certificate order flow and landed the first version of our DNS reconciler code, which I discussed above.
Mario's been working to get the front-end pieces of the certificate order logic landed, which he demoed on GitHub this week. It will land in 0.8 I think.
Dependabot updated a bunch of our deps. Good bot!
Toward 0.8
This week we're going to focus on paying down the tech debt I outlined above, and also try to get the last of our main features in place. Stefan, Tymur and others are working on adding an Admin UI and ability for admin users to assume the role of a regular user. Mario, Eakam and others are finalizing our UI to add missing copy, include instruction pages for users, etc. We also need to add the logic for disabling a user, finis implementing all the expiration and notification logic, add error UI, and dozens of other small things.
There's lots to do, but also lots that's already done. We're making good progress.
This week I worked on completing the order with the ACME provider.
After last week's work, I already had the order in a ready state, the last step was only to create the queue worker that finalizes the order.
During the last few weeks, a change in the DNS subsystem broke some previous steps, so as an additional task I did alter previous workers to follow the aforementioned changes.
During testing, we run into two problems:
The staging domain contained _ characters that are not permitted by Let's Encrypt
The DNS system does not currently work with multi-value RecordSets (Route53)
Both of these issues will soon be solved (I hope) so we will be able to test the complete certificate generation process (minus cleanup)
Speaking about cleanup, I'll probanly implement the final, cleanup step next week
Understanding the code base, writing good code, and flawless communication with maintainer are the fundamentals of the open source work. Luckily, I work with some of very experienced people in Starchart project. I learned that work can be faster when managing the work efficiently. Here's my lessons learned.
Minimize your work scope
If the PR has thousand line changes, it takes a lot of efforts not just for the code writer, but also the code reviewer. Also it means more chances of breaking existing code base. So it will take more than a week to get merged after rounds of reviews and updates. The longer the PR is under review, the more the PR gets conflicts especially if the project is very active. You can write sub issues of a large issue to minimize the work. If some parts of the issue is not the core of the work, you can separate for later follow-ups. Any work should be planned to finish within a week as project or organization usually is run on a weekly basis.
Split large scope of work into different phases
Planning big work into multiple phases is not easy task unless experienced. But this is something that I learned in Starchart project. The experienced code writer plans multiple steps for difficult tasks. I have seen the experienced developers build skeleton code without, write comments to show the intention, and structure the files without functionality yet. You can still have good code review feedbacks based on the function flow. Then slowly build up each module or functions. This way the work can be processed faster as you don't have to finish a large module at once. Instead you can finish your function, get it merged, and work on next function. I know it's not easy to plan things if the area of the code is new to the code writer. But I think it's a great skill and it's better to practice now.
Plan future improvement instead of fixing everything in one PR
Often times you get a feedback to change something during the code review. That change request can be essential but sometimes it can be improved later. Then you can suggest that you will write another issue to follow up. This way you can avoid another round of review and keep your change scope as originally intended.
Conclusion
When I work alone, I only focus on implementing features and fixing bugs. But larger project with a team requires more than implementing code. Design the system, continuously refactor code for better maintainability, manage issues and review someone else's code. And this planning the work will make a better look of the developer. The work progress is fast and easy to review. This technique will be more obvious in real world projects if you see PRs from the experienced.
describe() is a test suite, which can include many related individual test()s. Both let you include a string to state what it's for, which will show up in a formatted display when you run tests. beforeAll() is a block of code that executes before all tests of a suite is ran, which can be handy for setting up things, like creating an user for testing. afterAll() is a block of code that executes after all tests of a suite is ran, which can be handy for clean-up, like deleting all existing user.
Each expect() is a condition that must be satisfied for a test to pass. The code is more or like like plain English language, such as expect(user.createdAt).not.toBe(null); really means expecting user.createdAt to not be null. You can find all the syntax in their documentation.
You can put code, such as variable declaration an function calls, under describe() or test(). It's just a matter of scope for what you want to do.
Assuming you already setup the tool properly (see official guide), you can run the tests via npx vitest. A result can look like this:
For seeing test coverage, run npx vitest run --coverage.
As you can see, it tells you how much of the files are covered by tests, in terms of statements, branching paths, functions, and lines. It also shows uncovered lines.
Why do this?
This provides repeatable and standardized tests that are easy to run and view. You don't have to risk making mistake or forgetting a test like when one does manual testing of codes. The tool also provide good feed back by showing you what tests fail and what files aren't covered.
It's also something that can be shared in a repository and make collaboration easier.
An interesting way to use this is to write out some tests and have them act as technical requirements that guide the development of code. Essentially, you write out tests that tells developers what the software is supposed to do, and the developers write codes that would satisfy these tests that act as technical requirements.
In Starchart 0.6, one of the features we shipped is implementing repeatable background jobs to process expired DNS records at reconfigurable intervals on startup. Additionally, the jobs get removed automatically on completion and failure after seven days.
The Process
We start by running a query that fetches all the DNS records from the database, with the expiresAt field set in the past. If the array is not empty, we iterate through each record and perform the following actions:
Delete the expired DNS records from Route53 and the database
Send an email notification letting the user know about the DNS record expiration.
The Experience
This PR took less time to get merged than the one before it, as I am getting comfortable working with the BullMQ API. However, I should take some time to review my code to eliminate any ambiguity or silly errors, which I end up realizing immediately after pushing the code to be reviewed by the project maintainers. On a happy note, Starchart has attracted a new contributor, who raised a PR that closed one of the filed issues.
Amending my path
Over the last few months, it's my observation that I have improved in certain areas where I had struggled for a good chunk on my programming journey, and I could not be more happy about that I am amending my professional path.
The past week, I continued working on different issues in starchart. This involved adding a deactivated flag to users, which was a fairly simple change. This is required as we want to allow admin users to delete users. However, before a user can be deleted, we need to queue up deletion of any DNS records or certificates associated with that user.
This process can take some time, and we needed a way to prevent the user from creating any new DNS records or certificates while they are being deleted. Thus, we decided to add a deactivated flag to users and if this flag is true, they will not be able to perform any actions.
Another issue I worked on was to improve our documentation about Playwright, and more specifically, our configuration and how to use the automated report we generate after a CI run to debug any failing tests (wiki page).
While working on this documentation, I also worked with Ririio with his work on improving our e2e test coverage for various pages. We ran into an issue while testing the header as the links in the header move into a hamburger menu on mobile viewports:
Desktop View:
Mobile View:
Thus, as our playwright config ran the same test file for several browsers, including mobile view ports we would either have failing tests for desktop or failing tests for mobile.
An option was to skip tests if we were on a mobile viewport. However, due to an issue with mobile safari configuration, where it fails on GitHub Actions but passes on Windows (where I was able to test), we had to override the configuration's isMobile property to false as a workaround. So, this would not work. Even then, we would need to add a test.skip to every test we wanted to skip for mobile, and then add another test to only run on mobile.
After some discussion, we decided to use the testIgnore property in the configuration:
We added mobile specific tests in files ending with .mobile.spec.ts (eg: header.mobile.spec.ts), and desktop specific tests in files ending with .desktop.spec.ts (eg: header.desktop.spec.ts). Any common tests would still go in the usual file (eg: header.spec.ts).
This works as playwright has a testMatch property which by default matches all files using the regex .*(test|spec)\.(js|ts|mjs).
This week there were a few small PRs that I landed for our opensource project Starchart / My.Custom.Domain worth mentioning:
343 Which took out the SLO portion of our app in favor of just ending the session with Starchart
356 Which is a small change because the groups claim can come back as either an array or a string which was not something we were aware of and this fixes that. This is pretty important now as we're rolling things out into staging and need to give everyone the opportunity to log in but the change is small.
385 A version bump for the app to match the release notes for 0.6
TD and I also reviewed and merged a first time commiters PR for a simple typo here: 378.
I also gave a presentation on Wednesday about how my SAML code works, what the IDP and SP are, how their metadata is configured, the important parts of the configuration (the certificate, signature, claims and the assertion consumer service bindings) and how to follow along with the key requests in the SAML flow that have to do with what we send and what we receive at our callback.
The rest of the week's efforts on Starchart were spent on adding Single Logout back into the program and I have done it a few ways now.
The first time I gave the user a page with two buttons that were choices to either logout of Starchart or logout of all Seneca services, the review on the initial PR was that the session should automatically be destroyed and then the choice should be given to SLO. I decided to put my logout page under index because I wanted the pretty header to render with my buttons, this caused some problems here because destroying the session meant we had no user information any longer which meant the header could not render display name and if the user would choose to SLO we had no idea which user to SLO.
My first solution to get around this was to use global variables to keep track of the user that logged out while they were making their decision. It was later brought to my attention that globals won't work because they can't keep track of multiple clients usernames so I changed it to use cookies instead.
I added a redirect to the function that destroys the session and added a cookie for it to return with the username, in this way we can redirect to the logout page again and the loader can check for that cookie and use it as the value to send to the SLO request when the button's pressed and the post sets off the action to create the SLO request.
I made the username in the header optional although I think following recent discussions I will just move the logout page so it will not have the header since that improves clarity.
Hello Hello, this week was pretty productive and I got some good stuff delivered. The first thing I was working on was finishing with PR that I created last week, which was about the integration of the DNS record creation workflow into the existing UI. The second thing was a record editing integration and the last small thing was disabling actions if the record is inactive.
Polishing a big PR
It took some time and about 12 extra commits to target all suggestions on the PR I created the last week and eventually it got merged (very good).
Since we already had the DNS records workflow function to update records it was a good time to connect it with the existing UI for record creation, just by pre-populating values of the record that the user wants to edit. I cannot say that I faced any major challenges during development time. The biggest one was probably to keep my changes up-to-date with what was going on in the main branch since some of my peers worked on the adjacent part of the system. I also had a little confusion with David, when I did the same thing in my PR when tried to adjust my changes to the new vocabulary of the project. I ended up closing David's PR and just taking some ideas of his PR in mine (sorry David).
After the previous PR got merged I found that I was tagged in one small issue in GitHub, which was about taking away from users' ability to do any actions on records if they are not active. It was a quick 14 lines of code PR that are almost merged with no problem (I just made a typo in the error message, but everything else is good).
The next week I would like to work on making a server-side validation for records creation and editing that will return users with meaningful error messages for specific fields, where their provided values didn't pass validation. After that it would probably be good to make some layouts for the admin page, it will probably boost brainstorming from server-side people on how it should be implemented on their side.
Some afterthoughts
Even tho during almost the whole week (from the last Sunday to Friday) I was having a cold and was feeling really 💩 I was still more productive than the week ago because I was able to maintain more control over what I was working on by not wasting a lot of time and attention for social media. My war with myself for my attention is still going and I can already see the results of the first battles won. Peace ✌️
Strongly typed languages increase the safety of programs. It doesn't mean that strongly typed languages are always better than weakly typed languages. Weakly typed languages can reduce the development time. So programmers should choose a programming language based on the purpose of the program. Although I contributed some of Typescript projects, but I didn't have much knowledge about Typescript typing. I learned some of very useful typing techniques during Starchart code review.
When using the same type with different property names
You could face the situation that it doesn't make sense to have two different type names while only small portion of the properties varies. My DNS module in Starchart is a good example to explain this instance. At the beginning, we use the variable name as any type of domains. But in our app, this name could be subdomain or fully qualified domain name which can be shortened to fqdn. This is very confusing when writing a code to use the function. You don't have any idea whether you are going to get subdomain or fqdn which could result in a wrong implementation or frustrating the writer to look up all the code to figure what it actually is. So we can do something like this.
type Name = { subdomain: string } | { fqdn: string };
interface BaseDnsRequestData {
type: string;
value: string;
}
type DnsRequestData = BaseDnsRequestData & Name;
So DnsRequestData can have either subdomain or fqdn. When you extract the data, Typescript knows what type is passing only in runtime. So you should extract the data as below.
let name;
if ('subdomain' in data) {
name = data.subdomain;
} else {
name = data.fqdn;
}
When type naming is not necessary
Sometimes it's better not to name the type when passing data is obvious or varying similar names is more confusing. So you can instead of defining as below,
It was really good for me not just to learn these techniques, but also to help me to write better code. It seems not important for someone who develops something quickly, but typing is very important when many contributors or developers are working together. It finds the potential errors quickly and gives an idea of what this code does. It also takes a lot of learning from the experienced. I never thought about this kind of problems when I was working alone. Having the experienced developers' feedback is really helpful to learn how to write better code. Thanks for reading my article!
Testing has always been a major part of creating a stable project. Having a way to test major components of your work, ensures a stable development. For this week I learned writing tests using playwright. It's a testing tool design for an e2e testing with modern web application.
Changing it up a bit
For the past several weeks, I've been working on the front-end side of the project. Working with the Figma design, and then moving on to header and certificate implementations. This week however, I decided to go through the issue I was tasked on by one of my peers after each successful PR.
Testing was something I was quite afraid of tackling. I had some experience with my previous courses, but I never got to understand how it actually works in terms of E2E-- it turns out it was much more interesting that I thought.
Benefits of using playwright
Playwright provides developer a myriad of tools on how they create a test. What made me like it is the extension that it has for VSCode users. Of course not everyone would be using it, but hear me out for a second.
When creating your tests, unless you are fairly knowledgeable with what you are doing, you will consistently re-run them after each change, and hope that the next run would be a success. Of course a proper testing tool provides you with a reason as to why it fails, but the thing about playwright is that it allows you to visually see where the error occurs through the use of trace.
Trace allow us to view all the calls you make in the test, and show you where an error has occurs, whilst providing you with a visual presentation.
Another great tool is a video presentation that shows where an error has occurred. These tools are great to have for a much easier testing environment.
Another thing I want to address are specifically targeted towards VsCode developers. When working with playwright, they actually have an extension you can use to make testing much easier.
The extension allows you to run all or individual tests. It's a great tool to have when you're on the creating process
Pick a Locator - It's a great tool to have to allow you to see how to call a specific element on the test file as shown in the image below
Record New - It's a great tool when creating new files, because it records what you do on the page, and records it
Record at cursor - It's a water down version of record new, specifically used to do design to add new lines depending on what you do on the website
Those tools are mere addons that you can get when you install the extension. They are never perfect and may not give you the output you might need, but they are a great tool to help you ease the strain of creating a test.
Conclusion
Testing was and still is a fairly difficult portion when developing, but having great tools like playwright can help lessen the burden of writing a test.
My previous blog posts have chronicled my journey with ElasticSearch, starting from my early experiences using it in the Telescope open-source project. It's amazing to think that after almost a year, I still have the privilege of working with this powerful search and analytics engine as part of my job.
This month, I am fortunate enough to attend specialized ElasticSearch courses that are fully funded by my workplace. The courses are led by industry experts, giving me a unique opportunity to gain an in-depth understanding of ElasticSearch and its many features.
Although ElasticSearch is planned to be removed from Telescope, it seems fitting to share some of the key takeaways and new knowledge that I have gained. While I'm not sure how many posts there will be, I am excited to continue exploring and expanding my understanding of this powerful tool, especially in revising my explanation of how auto-complete indexing works.
Text Analysis
Text analysis is utilized in two scenarios: indexing text and querying search with text. This can be done through analyzers.
Anatomy of an Analyzer
In general, an analyzer can be considered as a bundled configuration for character filters, tokenizer, and token filters.
Sequential flow of text analysis:
___________ ___________ _________ tokens
| | | | | | ------> Index
Text --> | Character | ---> | Tokenizer | ---> | Token |
| Filters | | | | Filters | query
|___________| |___________| |_________| ------> Search
A tokenizer is the component that is in charge of separating a sequence of characters into distinct tokens (commonly words), and produce a stream of tokens as the output. It is worth noting that an analyzer must have and can only have a single tokenizer.
Besides being able to create Custom Tokenizers, commonly used built-in tokenizers include
Breaking down the Posts index in Telescope, we have
Two custom analyzers
One named autocomplete_search_analyzer that doesn't have any character filters or extra token filters, as it uses the built-in lowercase tokenizer
One named autocomplete_analyzer, which uses our custom tokenizer, autocomplete, which includes a customized setting for the edge_ngram type. Our analyzer also includes built-in lowercase and remove duplicates token filter
To be continued
For now, this is all I have time for. In later blogs, I'll continue to try and share my findings on text analysis for auto-completion, including insights on ngrams/edge-ngrams and index mappings.
As a continuation from the previous post, I managed to make the test database work.
It's not mocking anymore, but I'm keeping the title for consistency.
The Solution
Let's have a snippet of the dock-compose YAML file, which is what I used to make the test database, so it's easier to talk about this:
This file is an instruction that will be followed to create docker containers, when you run docker compose up command.
For this bit of code, it tells docker to:
create a container using image of MySQL version 8.x.x
name it "database"
bind port 3306 (left) to the container's port 3306 (right)
use local machine's directory (left) as source of container's virtual directory (right), such as ./mysql-data:/var/lib/mysql
set certain environment variables
MYSQL_DATABASE indicates the database to be made with the name starchart. MYSQL_USER and MYSQL_PASSWORD are the account and password with superuser access to that database. MYSQL_ROOT_PASSWORD sets the password for the root user, which is a super user. Only this environment variable is mandatory.
SQL File for Test Database
As you can see in that code snippet, I source a file 01-databases.sql, which as a few SQL commands to setup a test database on top of starchart database. The file looks like this:
-- create test databasesCREATE DATABASE IF NOTEXISTS `starchart_test`;
-- create root user and grant rightsCREATEUSER'root'@'localhost' IDENTIFIED BY'root_password';
GRANTALL PRIVILEGES ON*.*TO'root'@'%';
First line creates the database if it doesn't exist already. The second line creates that user with specified password, and then it grants all access to that user.
When the MySQL database container is made, Docker checks for SQL files under /docker-entrypoint-initdb.d to run, so these lines would be executed.
What was the issue?
It turns out there were two issues with my connecting string to the test database.
As an example: DATABASE_URL="mysql://root:root_password@127.0.0.1:3306/starchart_test"
starchart is the account to use to connect. root_password is the password for the account. 127.0.0.1 the IP of the database. 3306 is the port. starchart_test is the database name.
First, your system might refuse to connect to a high number port to avoid suspicious ports, so I needed to set it to be lower. A MySQL database docker container lists 2 ports: 3306 and 33060. I was trying to bind the 33060 port, which was too high, so a lower number port did the trick. Another issue was that I had the wrong name for the database. I mistakenly thought it was meant to be container name instead.
This week I worked on the third step of the DNS generation flow, verifying the challenges with the ACME provider.
After the 2nd step is complete, and we know that DNS propagation is complete, we can take the next one and alert Let's encrypt about this fact.
In response to this, the provider will do it's own async background process of verifying the same dns records. In the meantime we need to keep checking the challenge status with them, and once each challenge is verified, we can complete this job and move to step 4. This means that this step is a verification trigger, and a guard step, combined together.
Next week I'll work on the next flow step, which actually finishes the certificate order, creating the certificate.
I went with a similar setup to the setup you would have for multiple roles with a shared account across all tests that require login. Currently, all the pages do not differentiate between roles, i.e., they do not show different things based on the role. However, a design for an admin dashboard was being worked on, and will be implemented in the upcoming weeks. So, I decided to use a setup that would be easy to expand for different roles such as an admin user.
I started by adding a setup file under my e2e tests folder which would go through the login process, and save the authenticated state in a file:
import { test as setup } from'@playwright/test';
const userFile = 'test/e2e/.auth/user.json';
setup('authenticate as user', async ({ page }) => {
...
// Login stepsawait page.context().storageState({ path: userFile });
});
Then I modified playwright config and added a new project that will be a dependency of all other projects. A project has a name and defines configuration for running the tests on a specific device in this case:
By setting setup as a dependency, it will be run before the tests in the project are run. testMatch defines a regex for the setup file(s), and it matches paths relative to the test directory defined in the config.
Since the setup process is run before the tests are run, we actually don't need to commit our file with the saved session to git. Thus, I added it to the .gitignore file.
I also added tests for the New DNS Record form. This form is used by a user to create a new DNS Record. Since this required a user to be logged in, we can use the saved session state generated in the setup project:
test.describe('authenticated as user', () => {
loggedInAsUser();
Here, loggedInAsUser() is a util function that was added based on PR reviews:
test.use can be specified in a describe block or in a test file. It is not necessary to use this in a beforeEach or beforeAll, and doing so would result in an error.
I ran the tests locally, and they all passed. However, I encountered a strange error in CI (which uses GitHub Actions) for the Mobile Safari project:
Test timeout of 30000ms exceeded.
page.goto: Navigation failed because page was closed!
Playwright generates a report containing a video and a trace on the first retry, depending on the configuration. I looked at the video and saw that most of it was a blank black screen. After trying a bunch of things such as upgrading playwright, and commenting out all other projects, I found this open issue on the playwright project. There was a super helpful comment left on the issue with a temporary workaround. Basically, setting isMobile to false for the iPhone configuration used by the Mobile Safari project results in the CI passing on GitHub actions.
This week I have been working on two major things, which are:
UI design for the admin panel page
Records creation, prolonging and deletion flow integration with front end
Design
People from Seneca requested our team to include some sort of admin panel in the startchart and as the first step of doing so, we needed some ideas on its design of it. What I liked about developing design first with the close collaboration of all team members is that it stimulates everybody to think about how certain functionality is gonna work on all layers. I presented my ideas on the design on Wednesday for the team and as I expected we had a very productive conversation on not only the design itself but also on how the admin functionality is gonna be implemented in general. As a result, we got a pretty decent concept of what UI design is gonna include and how it is gonna be done.
Records flow integration
The day has come and I finally created the PR on integrating the records creation flow which includes not only creating the record in DB but also manipulating with Route53. Since the flow was implemented on the back end side I was able to integrate it on the front end. I faced one interesting issue while doing so.
That issue was when I tried to delete records which were created by seeding DB and had an issue with doing so. At a certain point with the help of Won I realized that since we create the fake hosted zone after seeding DB these records are not in fake route53 but only in the database. This made me integrate record creation first, so I could work with other manipulation on them. I also deleted seeding DB with that records, because it provides more confusion than benefits.
UPD: looks like E2E tests are failing because of removing tests, maybe I should do something else
Conclusion
It was a nice week, especially the few last days. I started proactively fighting against my huge social media addiction and it helps to make more stuff done and just feel better in general. My focus is returning to me and I feel like I can start to look deeper into things. Hope the next week is gonna be even better. Peace ✌️
This week, I upgraded the notification system in Starchart by implementing background jobs that repeat every 5 mins to query the database for existing DNS records or Certificates expiring in less than a month. If the query returns any data, we check the last time we notified the user. If we have never notified the user, we will send an initial reminder encouraging the user to log in to the system and renew the expiring records.
I am confident that initial reminders will be rolled out. Still, we would need to update the value of lastNotified when we renew expiring DNS records and Certificates so that users can be reminded again. I need to update the code so we can send a second reminder when records are about to expire in less than a week.
Implementing background jobs to roll our notifications in this PR has been one of my most meaningful contributions to the project. It needs improvement, and I intend to perfect it so that any code that builds on top of it does not fail.
This week I had a chance to talk with Philip Guo, an associate professor at UC San Diego. He's been interested in how CS/programming instructors are dealing with the influx of LLMs, like ChatGPT/Copilot, in their classrooms. After talking with more than a dozen people in the US, he saw my "CheatGPT" post on Hacker News and reached out for my Canadian perspective.
I wanted to write down some of what we discussed, since the many AI conversations I've been having recently are forcing me to ask myself important questions–I still lack answers, but the process is helping me get closer to where I want to be.
We started with how I've used these tools. What I've found most compelling so far as been their ability to help me accelerate difficult tasks. For example: writing distractors to multiple-choice questions or developing similar types of problems for students to work on, without repeating any of the core ideas (i.e., "I need a question like this one, but make it different"). Philip talked about the idea of something being easy to recognize, while hard (i.e. tedious) to generate. I also talked about my experiments building an infinite API backend, which strictly adheres to JSON formats. I've also been playing with the new Chat completions API to see what I can do with the extra System Prompt–so far I've been having good success building a chatbot that helps teach students about coding concepts, but never gives them actual code snippets.
I also talked about one of my big worries with early semester CS students, namely, that they will overlook the value of going slow while learning, mistaking AI's speed with their own ability. I don't have a great way to describe this idea yet. I'm fascinated with how tasks that seem tedious can feel slow for different reasons. I asked Philip if he ever uses AI when coding: "I tried Copilot when it came out, but at some point I lost access and never felt compelled to go restore it." This is largely how I feel, too. I don't really need AI to write code. It's neat that I can, but in languages where I'm proficient, I can already code at about the same speed that I can type.
So what about a language where I'm not proficient? What if I put myself back into the position of my students? Here I run into a new problem. How do I judge the quality of the code I'm being presented? I see lots of people on Twitter enjoying themselves as they prototype new apps in an afternoon with LLMs: "I don't know any Python and I just wrote this!" I think that's fantastic, and I need to try doing it myself. But I also know that any software you plan to use beyond prototyping requires all kinds of verification, contextual and domain knowledge, security considerations, bug fixes, etc. ChatGPT and Copilot are just as likely to make things up and miss important details when writing code as we see with prose hallucination. I saw someone online discussing how Copilot had written code with a division by zero error, and another showing how it had confused seconds and milliseconds in an algorithm. There's a reason why even the best programmers get their code reviewed by each other. Writing a correct, secure, fast program is hard!
Speaking of code review, I already do a lot of it with my students. What I've experienced in my open source courses is that, for the most part, students don't know how to do it (yet), and are largely afraid to try (for fear they'll miss things or reveal their own shortcomings). Running your eyes and mouse over a piece of code isn't code review. However, a lot of the reason why students struggle to do this well is that they are taught to write, not read code. We haven't prepared them for it.
Philip and I talked about how literary educators often rely on having their students read texts much more complex than anything they expect them to write. I've always subscribed to this approach, requiring my students to work on and in code that is beyond their current skill level. I've always believed that if you want to become a better writer, you should focus on reading more than writing. But if our current students aren't being taught to wrestle with large code as a reading exercise, how will they cope with AI generated code that may or may not be valid? The reality is that new developers don't have the capacity to debug problems in code they already don't understand. "Look at how much of the code it just wrote for me!" Amazing. How many bugs did it include for free? Without a major shift in the way we teach, moving from write-only to read-and-write, I don't see how learning to program is compatible with AI dropping 25-50 lines of code in your editor. Text isn't a reusable component. No one can be handed a bunch of text as-is and hope to combine it with what they already have.
We also discussed the loss of traditional approaches to assessment. One of my favourite assignments for my programming students is to ask them to write various functions, giving them a set of tests and a comment block that describes how the function should work in prose. The difficulty I see now is that what I've just described is a prompt for learning, and hence, a prompt for ChatGPT and Copilot. As a result, marking assignments has become a Turing Test: did you or the AI write this code?
Back to what I was saying about tedium. I need to try and understand how to differentiate "slow" as in "I'm learning how to do this and I'm not very fast yet" from "slow" as in "this takes forever, despite my skill level." We're told that AI is here to free us from the latter. But in an educational setting, where students are slow in both senses, I need to find compelling ways to help them understand how to build patience as they struggle to acquire new skills. And I need to do this while not withholding the power of AI to free them from working on pointless exercises. To pretend that AI has no place in the classroom is ridiculous. But what are its limits?
I told Philip that I'm planning to have my open source students work with the ChatGPT API in the fall (or whatever it's called by then), building programs that leverage LLMs to solve their own problems. I want to see what students would do if they were allowed to work with this technology outside of issues of academic integrity. I think the only way I'm going to get closer to understanding how to sort this out is to plunge myself into something beyond my own skill level. I also think I need to do it with my students vs. coming up with a plan on my own.
I'm enjoying the many chances I've had lately to talk with people about these issues. There's so much to consider.
This week was the first coming back from the reading week break and I had a few clear cut objectives coming into it. The first thing that I wanted to accomplish was to create an e2e test for the authentication flow that I have added to the open source starchart project. The library that we use for e2e testing is playwright, and using it is actually pretty easy. The corresponding issue for the creation of the test is here. Initially there was a speed bump seeing as the playwright scripts were written to run with a different port but David looked into it and came to the conclusion that using 8080 should be fine and after that change my e2e tests passed CI PR#1 to my branch.
I learned after I was done with creating my test that it is possible to use a plugin to easily record things but doing it how I did it was not far off, instead of using a pretty plugin I just ran the dev server in one console and I ran an npx command in another: npx playwright codegen localhost:8080. This looks a little wonky seeing as for some reason the CSS does not quite take on this browser that is used for recording tests but in any case it makes writing tests easy because you can practically do what you want to test and it records the playwright syntax that would re-produce your actions on the specified URL (in this case localhost:8080). Here's how it looks:
along with a console that allows you to pick elements out of the page and start and stop recording along with a real time view of the playwright syntax you are recording with your actions:
Something similar to the auth test that I wrote in #294 was later used as a dependency for the rest of the e2e tests in a PR written by Eakam #313.
The second order of business this week was to bring the local idp I set up more in line with the staging idp by actually testing our SAML in staging and seeing what claims we get back from Azure AD and then taking those claims and configuring our simpleSAMLphp local idp to return the same claims.
David tasked me with gutting starchart so that all that was left was the SAML logic basically and so that is what I did. Then after the second meeting of the week we got on a call and took my butchered branch to staging after I pushed it as an image to dockerhub so that he could run a container of it.
There was a problem that I did not understand when we got to trying to run it, to solve it David pulled up the files in the build folder and added logging to key points where our IDP was being used to find that despite us setting the IDP in an init method when the code called for its use it was undefined. This was the impetus for the changes to the init logic that David had setup the weeks prior in #321. The change towards loading the idp metadata from a file turns out to also be the recommendation from a security perspective as well apparently as mentioned here.
After fixing the issue we were able to see what the response from the actual Seneca IDP would look like:
I was also able to take some snapshots of our great victory:
The work we did here allowed Chen and I to rework the user model and simpleSAMLphp config in the following #312#307
Looking forward
I noticed that the routing is deprecated and slated to change to flat routes and opened the following issue #320.
I also opened an issue to do with how we plan to handle admins, in that the admins are meant to be able to assume control of regular user accounts and so there should be an "effective username" field as well #306. The effective username should come in empty only for admins and the rest of the users should have that field be populated with their username.
I wrote a post roughly 2/3 years ago regarding data structures and algorithms. I thought I'd follow up with some questions I'd come across such as the famous Blind 75 Leetcode Questions and write down what my thought process to approaching the problem + run time and space complexity of the solution for the given question.
I usually solve all my questions in Python. The first of which questions we will work on is Contains Duplicate:
Regardless of the approach, we'll have to check every number in "nums" so our run time will be O(length of nums) at a minimum. We'll also assume within nums there is only one possible duplicate.
What's the most intuitive solution when we read through this? Most likely checking the current number n against all other numbers (n+1, n+2, n+3...).
We'll use the first example: we're on the first element, 1. We'll have to check 1 against 2, 1 against 3, 1 against 1. Then we'll check 2 against 3, 2 against 1, we don't have to check 2 against the initial number because we have already made that comparison before.
So we'll come up with something like the below:
Code is pretty simple, we use a for loop to iterate through nums and within that loop, we'll do another for loop to check elements of n+1 until we reach n + length(nums) - 1. This results in a O(N^2) solution because for each iteration of nums we're roughly going through it length(nums) - index times. The space complexity of this is O(1) since no extra structure is used to store anything.
Is there anything we can do to reduce the number of times we have to iterate through the list? We can perhaps use a dictionary, a key/value store. We will use the number as the key and a value depending on how many times we have come across the number. Using Example 1:
first iteration, the value of element at iteration 0 is 1, does the key 1 exist in the dictionary? No, we'll add it and set the value to 1
second iteration, the value of element at iteration 1 is 2, does the key 2 exist in the dictionary? No, we'll add it and set the value to 1
and so on until we hit iteration 4, does the key 1 exist in the dictionary? Yes! we'll return True since the key exists and this means we have found a duplicate. In another scenario if we iterate through the whole list and no duplicates are found we'll return False.
What is the complexity of the above code? O(N) time-complexity since we're only iterating through the array once (we have to create a dictionary from nums). O(N) space-complexity since we'll need to store values up to length nums in the variable my_dict.
Is there a more concise way to do this? Actually there is, we can use a structure called Sets, a set in Python is similar to a list (array) except it does not allow duplicate values.
Given a set cannot contain duplicate elements, we can compare it to the original list, if both are the same we return False, otherwise if they're not the same that must mean the nums must contain a duplicate. We arrive at something like below:
What we're doing in this case is comparing the length of nums against the length of nums turned into a set. If there are no duplicates both lengths should be equal and we'll get a value of 0 (False). Otherwise we'll get a value of 1 (True) since the length of the set nums cannot contain any duplicate values the relation between the two lengths would be something like this: length of nums >= length of set(nums).
What is the complexity of the above code? Same as the dictionary solution, as we'll only iterate through nums once and we'll have to create a set of length nums (roughly) to store values up to length nums.
This week I have worked on the internal challenge verification process.
Once we create an order with the ACME provider, we have to complete a number of challenges. For our peoject, we chose the DNS verification method.
For this to work, we have to verify DNS propagation before we send the challenge completion request to Let's encrypt.
An additional criteria for this step (that I opted to make) is that it should not require the use of any API calls towards the provider for rate-limiting reasons.
Because of this, I implemented a static function on the Acme class I created, that uses the authoritative dns server to request data directly from the Route53 service about dns record availability.
This week I tied this functionality to the appropriate DNS worker. This "guard" step should delay all subsequent steps until the DNS server is ready for challenge verification (by the ACME provider).
Next week I plan to implement notifying the provider about the DNS challenge being ready to complete.
Working with other people can be daunting. You are always afraid of what to say; thinking that if you are wrong, you'll be looked down upon by everyone. While, it might happen, and let's be honest not everyone you will work with will be a saint, you shouldn't let that prevent you from saying what's on your mind.
When you're on a meeting, asking your peers to explain a topic you have no understanding of isn't a bad idea. You might not be the only one who feels the same way. By asking a question, you are allowing everyone to be on the same page, and not just those who are talking.
Never ask to "know", alway ask to "understand"
Let's put you on a scenario. You are new to coding, and in this week's lecture your professor talks about switch case. By the end of the lecture you were confused, you asked your professor, "what does switch do?" they provide you with a detailed answer, and be done with it.
A few days later you came across a problem when writing an assignment, when you wrote your switch, you had problems with your code continuously going through all condition even after it went through the first one. Now you're in a predicament, and confused again as to what to do. You asked your professor again, and he explained that you are meant to add a break at the end of each case. The entire problem could have been avoided if you simply ask "how does switch work"? Of which you will be provided with multiple scenarios, and how the entire process goes about.
Of course with a question regarding "switch" it is very unlikely for anyone who has a low-mid level of understanding of programming to not even mention something as small as "break". However, if you consider topics that are much more complex, and require deeper understanding, the impact of your question can drastically affect how the information is going to be provided.
Questions leads to even more questions
During a meeting this week in regards to a project. We were discussing about admin's ability to control a student's account.
While everyone was discussing a myriad of things in regards to UI and logic I noticed a problem. I recall they mention that the deletion of an account follows a sequential order dns-records -> certificates -> user, that's when a question popped into my mind "what would happen when the user creates a new records just as they are being deleted by the admin?"
I was afraid of asking it at first. "What if I'm just overthinking? The likeliness of that scenario occurring is close to 0, and I'll probably look dumb," I thought to myself. But I swallowed up my fear, and waited for an opportunity to ask my question. It turns out that we didn't even considered the possibility, and such problems did occur in past projects according to our leader, so now we are developing a deterrent. It wasn't a huge issue, but it did provide me with an insight as to the importance of asking questions.
Conclusion
Always ask questions with the mindset of wanting to learn; it helps you deeper your understanding of the work instead of simply knowing the answer to it. When you have a question, it is never bad to ask. If it's simple, then the answer would take only a few seconds(not a lot to deter the entire discussion). At the end of the day, us developers are in a constant state of learning, questions help us deeper our understanding of the digital world we dipped our toe in.
What I did this week was matching User table of database in Starchart project with what useful information SAML would return about users when users log in.
This meant adding group column in User table in database, given it's one of the claims. group claim is used as role assignment, such as user being part of student group, which has specific access attached to the group.
This project uses Prisma to interact with the database.
There were 3 things I needed to do:
Edit the schema file (prisma\schema.prisma).
Edit the file (prisma\seed.ts) for seeding to make sure these seeded users have groups.
Chang how relevant functions interact with the database, so a function that make Prisma API call to create a user would need to be changed for the addition of group. When changing a function definition, one needs to remember to change how the function is used in other area of your code. For example, I changed the parameter of createUser() to allow argument for group, so I must change how the function is used elsewhere in the project too.
Cover new use cases for the column. This meant I added 3 functions to check whether a user belongs to one of 3 groups. The functions isStudent(), isFaculty(), and isAdmin() accept username as parameter. They would return true if they have the role/group, false if not. The code look something like this:
exportasyncfunctionisFaculty(username:
PrismaUser['username']) {
const { group } = await prisma.user.findUniqueOrThrow({ where: { username } });
// The group will have -dev in it on staging but not on prodreturn/mycustomdomain(-dev)?-faculty/.test(group);
}
async marks this function as asynchronous, since it needs to interact with the database and await a response.
The group value will be returned by the Prisma function after it fetches the user, by using the primary key username. Notice typescript syntax is used to limit the type of username according to Prisma User model's username column type.] findUniqueOrThrow() will throw error if nothing is found, as the function name suggests. You can see the documentation here.
After group is returned, a regular expression is used and test against the group value. If there's a match, it will return true, or false in the case of no match. This looks for any mycustomdomain-faculty or mycustomdomain-dev-faculty in group.
Consider other adjustment to codes relating to User. There was no need for additional adjustment for this, but I could easily needed to do so. For example, if I changed a column name, I would need to change other references to the name in the project.
Also, I merged firstName and lastName columns into one displayName column, given that's what SAML gives back. The same ideas for such changes in database schema apply: edit the schema file, edit the seeding file, change/add relevant functions, and other relevant code adjustment. In this case, no new function was added.
Change Log Of The PR:
Add group in User model as String in prisma\schema.prisma
Add examples in prisma\seed.ts
Modify createUser() in app\models\user.server.ts accordingly
Add isStudent(), isFaculty(), and isAdmin() in app\models\user.server.ts.
Combine firstName and lastName into displayName
Modify arguments of createUser() in app\routes\login\callback.tsx
isStudent(), isFaculty(), and isAdmin() accept username as parameter. They would return true if they have the role/group, false if not.
Our open-source project Starchart uses Prisma which is ORM for any JavaScript and TypeScript projects. As it states on the doc, it's the next generation ORM that provides a lot of convenient features. I'm really amazed how it makes things easier our project. Let me show how to use Prisma briefly in this blog.
Getting started
Once you have your project ready, you can install Prisma with below command.
npm install prisma --save-dev
And initiate Prisma in your project. You can choose your data source like SQLite, PostgreSQL, MySQL, MariaDB, MongoDB, MS SQL server, etc. In this case, I choose MySQL.
npx prisma init --datasource-provider mysql
Writing schema and init PrismaClient
Now it's ready to use Prisma. Let's set up some schemas.
model User {
id Int @id@default(autoincrement())
email String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
public Boolean @default(false)recordRecord[]
}
model Record {
id Int @id@default(autoincrement())
username String
description String @db.VarChar(255)
user User @relation(fields: [username], references: [username])
}
This is an example to show how to write Prisma schema. You can see how to set ID column, make a property unique in the table, set initial date property when created, as well as default value and length limitation. And the table relationship between User and Record. You can find more details on Prisma docs Defining attributes.
CRUD operation
Next step is to create a new SQL migration file and run the SQL migration file against database. Below command creates dev.db inside the prisma directory.
npx prisma migrate dev --name init
Now you need the PrismaClient initialization script.
You can also use methods like findUnique, findFirst, updateMany, upsert, deleteMany, count, etc. If you have a bit of SQL experiences, you would understand what each of these command does. Let me leave a link to take a look at further details Prisma CRUD operations.
Seeding and Prisma Studio
What's great about Prisma is the various tools that makes your development easier. You can set up a seed script to create initial development data. In order to do so, you need to add a prisma script as below.
//In package.json for Javascript project{"name":"starchart","version":"0.0.1","prisma":{"seed":"node prisma/seed.js"}}
For Typescript, you need to install @types/node and add Typescript version script in package.jon.
Then you can run npx prisma db seed to seed your database.
Prisma also provides the visual editor for easier data view/edit.
You can run npx prisma studio to see the visualized tables.
Conclusion
Developers need to work with database not just to write CRUD functions against database, but also database connection based on environment, create/change schema, migrate database, seed development database. Prisma can help you do these jobs easy in a sophisticated way. The fastest way to get familiar with Prisma is to go through the tutorial on the official website. I hope my article is helpful to know Prisma.
For week 7, I continued with my work on the DNS Record creation page. This basically involved resolving all the review comments, and trying to get tests working. However, I ran into another sign in problem in the Playwright test environment:
I did not want to resolve this specific issue in the same PR as the one creating the page so I decided to create a separate issue for it.
While working on the DNS Record creation page, I rebased my branch with the main branch as it had gotten out of date. After this, I found that I could no longer run the app, and got this error:
'SECRETS_OVERRIDE'is not recognized as an internal or external
command, operable program or batch file.
I saw that the dev script had been modified to override an ENV variable:
"dev":"SECRETS_OVERRIDE=1 run-p dev:*"
However, this syntax is not supported on Windows. So, I created a PR to use cross_env so it would also work on windows.
After addressing all the comments on my original PR (to create the new page UI), I helped Genne23v resolve CI failure on this PR. New code had been added that would check for an ENV variable value and crash if the environment was production and no value was specified. However, this should not have affected the end to end tests as the environment there was set to testing:
This script starts the app when end to end tests are run. However, after some investigation and through this discussion, I found that there were two problems with this approach:
testing is not a valid value for NODE_ENV. Providing testing will result in NODE_ENV being set to production
During the build process, all calls to process.env.NODE_ENV in the code are replaced with the actual value for NODE_ENV. Thus, something like console.log(process.env.NODE_ENV) would become console.log("production"). This was done by the following script. Here, no value for NODE_ENV is given, and therefore production is assumed by default.