<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yoshi</title>
    <description>The latest articles on DEV Community by Yoshi (@yo-shi).</description>
    <link>https://dev.to/yo-shi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png</url>
      <title>DEV Community: Yoshi</title>
      <link>https://dev.to/yo-shi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yo-shi"/>
    <language>en</language>
    <item>
      <title>PTA Digital Transformation with Google Workspace: A One-Year Journey</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Thu, 18 Jun 2026 02:52:12 +0000</pubDate>
      <link>https://dev.to/yo-shi/pta-digital-transformation-with-google-workspace-a-one-year-journey-2d27</link>
      <guid>https://dev.to/yo-shi/pta-digital-transformation-with-google-workspace-a-one-year-journey-2d27</guid>
      <description>&lt;p&gt;Since April 2025, I have been handling IT operations for our PTA. The PTA uses Google Workspace, and there was a volunteer position called “Account Administrator” that required PC skills. Thinking it would involve managing accounts and working with spreadsheets, I applied for the role. However, the reality turned out to be quite different.&lt;/p&gt;

&lt;p&gt;While Google Sheets was indeed being used, most of the work relied heavily on manual processes, which was honestly discouraging. A thick operations manual had been prepared, covering everything from monthly tasks to procedures performed only once a year. Some of the original documentation referenced throughout this article remains in Japanese as it was created for local PTA operations.&lt;/p&gt;

&lt;p&gt;Over time, I started building small tools to gradually reduce the monthly workload.&lt;/p&gt;

&lt;p&gt;Reading through handover documents was difficult. Distributing information to individual classes was tedious. Tracking the status of inquiries was cumbersome. I wanted to eliminate as many of these repetitive manual tasks as possible. Fortunately, I had some experience with cloud technologies and databases, which allowed me to experiment and gradually build a collection of cloud-based tools centered around the Google ecosystem.&lt;/p&gt;

&lt;p&gt;In this article, I will look back on what I built over the course of a year, the order in which I built it, and the reasoning behind the overall architecture. Originally, I intended to write this retrospective at the end of my term in late March, but as often happens, I kept putting it off. Eventually, I found myself publishing it at this somewhat unexpected time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;April 2025

&lt;ul&gt;
&lt;li&gt;Started developing a suite of PTA operational tools&lt;/li&gt;
&lt;li&gt;Consolidated handover documents and operational knowledge using NotebookLM&lt;/li&gt;
&lt;li&gt;Built a bulk Google Drive distribution tool&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;May 2025

&lt;ul&gt;
&lt;li&gt;Implemented a workflow that automatically adds users to Google Groups from a mailing list registration form and sends confirmation emails&lt;/li&gt;
&lt;li&gt;Added automatic ticket numbering, acknowledgment emails, and Google Chat notifications to the inquiry form&lt;/li&gt;
&lt;li&gt;Created a simple ticket management interface to track inquiries and their resolution status&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;June 2025

&lt;ul&gt;
&lt;li&gt;Established the core PTA registration workflow, including parent registration, new entry processing, and notification delivery&lt;/li&gt;
&lt;li&gt;Built the data management foundation using BigQuery tables, views, and stored procedures for membership and role information&lt;/li&gt;
&lt;li&gt;Developed a web application for browsing the PTA directory, including search, filtering, and export functionality&lt;/li&gt;
&lt;li&gt;Officially adopted Google Chat as the primary organizational communication platform&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;July 2025

&lt;ul&gt;
&lt;li&gt;Evaluated the effectiveness of a volunteer assignment preparation tool for library duty scheduling&lt;/li&gt;
&lt;li&gt;Improved the process for synchronizing Google Group memberships based on BigQuery data changes&lt;/li&gt;
&lt;li&gt;Developed a tool that automatically generates cultural material loan request forms and distributes them as Excel files&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;August 2025

&lt;ul&gt;
&lt;li&gt;Added support for PTA role changes and various response-processing workflows&lt;/li&gt;
&lt;li&gt;Automated the parent information update pipeline&lt;/li&gt;
&lt;li&gt;Migrated and launched the PTA website within Google Workspace&lt;/li&gt;
&lt;li&gt;Added automated Google Analytics 4 reporting and aggregation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;September 2025

&lt;ul&gt;
&lt;li&gt;Established a local development workflow using Apps Script and clasp&lt;/li&gt;
&lt;li&gt;Improved user-facing directory update notifications and export functionality&lt;/li&gt;
&lt;li&gt;Began designing and implementing a PTA membership fee collection system&lt;/li&gt;
&lt;li&gt;Created BigQuery tables for billing and payment tracking and started the initial Stripe integration&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;October 2025

&lt;ul&gt;
&lt;li&gt;Completed the core implementation of the payment collection system&lt;/li&gt;
&lt;li&gt;Added Stripe Checkout, webhooks, payment verification, signed payment links, billing emails, and payment reminder workflows&lt;/li&gt;
&lt;li&gt;Refined the BigQuery schema and improved deployment and operations with Cloud Run and Docker&lt;/li&gt;
&lt;li&gt;Continued improving parent information updates and directory synchronization accuracy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;November 2025

&lt;ul&gt;
&lt;li&gt;Focused primarily on ongoing operations and minor enhancements&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;December 2025

&lt;ul&gt;
&lt;li&gt;Implemented a volunteer recruitment form for library duty assignments&lt;/li&gt;
&lt;li&gt;Added support for multi-slot selection, instructional messaging, and volunteer notification emails&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;January 2026

&lt;ul&gt;
&lt;li&gt;Developed selection logic for next year's officer nomination process&lt;/li&gt;
&lt;li&gt;Created SQL queries and Google Apps Script tools to support officer selection&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;February 2026

&lt;ul&gt;
&lt;li&gt;Built the full library duty assignment system&lt;/li&gt;
&lt;li&gt;Implemented a family ID–based registration interface, first-come-first-served and lottery assignment logic, recruitment period controls, directory views, and shift replacement workflows&lt;/li&gt;
&lt;li&gt;Enhanced the membership fee system with additional interfaces, including payment completion reports and billing email logs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;March 2026

&lt;ul&gt;
&lt;li&gt;Improved the library duty replacement workflow&lt;/li&gt;
&lt;li&gt;Refined replacement requests, candidate selection, user interface elements, notifications, and authentication processes&lt;/li&gt;
&lt;li&gt;Developed a customized email distribution tool for targeted user groups&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Looking Back, the Year Fell into Three Distinct Phases
&lt;/h2&gt;

&lt;p&gt;Looking back, the improvements made over the year can be broadly divided into three phases.&lt;/p&gt;

&lt;p&gt;The first phase, from April to May 2025, was about replacing individual manual tasks. I built tools for consolidating handover documents, distributing files to class shared drives, handling inquiries through a help desk system, and managing mailing list registrations. At this stage, the goal was not to integrate data across systems but simply to eliminate recurring weekly and monthly manual work one task at a time.&lt;/p&gt;

&lt;p&gt;The second phase, from June to October 2025, focused on building a foundation. The challenge shifted toward managing information shared across multiple business processes: membership directories, parent registrations, officer assignments, mailing list synchronization, access control, and payments. This was when BigQuery entered the picture. Google Group updates became automated, access permissions were organized, and responsibilities that spreadsheets alone could no longer support gradually moved into a separate data layer.&lt;/p&gt;

&lt;p&gt;The final phase, from late 2025 through March 2026, was the transition toward an operational platform. Volunteer recruitment, shift scheduling, replacement requests, notifications, and campaign-style email distributions gradually evolved from simple forms into services that users actively interacted with on an ongoing basis. By this point, the collection of tools felt less like a set of convenient scripts and more like a small ecosystem of business applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  It All Started with Eliminating Painful Manual Work Using GAS
&lt;/h2&gt;

&lt;p&gt;My first impression when I took over directory administration in March 2025 was simple: &lt;em&gt;this is incredibly manual&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Although the PTA had already adopted Google Workspace, nearly every operational process had been designed around manual work. To be fair, that may have been unavoidable given the nature of a PTA organization. Most positions rotate annually, and procedures are intentionally designed so that anyone can perform them regardless of technical background.&lt;/p&gt;

&lt;p&gt;The first tool I built was a handover document consolidation tool.&lt;/p&gt;

&lt;p&gt;Handover documents were created every year, but their quality and level of detail varied significantly depending on who wrote them. Some years were excellent; others contained only minimal information. To make the accumulated knowledge easier to access, I created a system that searched Google Drive for &lt;code&gt;.docx&lt;/code&gt; handover documents scattered across folders organized by year and class.&lt;/p&gt;

&lt;p&gt;The tool extracted documents whose filenames contained the Japanese term for "handover," collected the contents into a spreadsheet, and organized them by school year, grade, and class. I then used NotebookLM to generate summaries and compile frequently asked questions from the collected material.&lt;/p&gt;

&lt;p&gt;This was not a flashy automation project, but it solved a recurring problem: every year, someone had to spend hours figuring out where information was stored and what had been done previously. Since both the source data and output destination already lived within Google Workspace, Google Apps Script (GAS) was the obvious choice. Being able to work directly with Drive, Docs, and Sheets within a single environment made development remarkably straightforward.&lt;/p&gt;

&lt;p&gt;The next tool was a bulk distribution system for class shared drives.&lt;/p&gt;

&lt;p&gt;The organization supported students ranging from preschool through high school, resulting in roughly forty shared folders organized by grade and class. Previously, documents were distributed manually, one folder at a time, with a surprising amount of care and effort devoted to what was essentially repetitive file copying.&lt;/p&gt;

&lt;p&gt;It was not difficult work, but it was undeniably tedious.&lt;/p&gt;

&lt;p&gt;The new tool allowed documents to be distributed to multiple class shared drives at once. PTA operations frequently involve sending the same file to several classes or committees, and repeating that process manually increases the risk of missed deliveries or accidental distribution to the wrong destination. To address this, I automated the entire workflow: identifying recipients, distributing files, recording logs, and archiving the distribution history afterward.&lt;/p&gt;

&lt;p&gt;The design philosophy during this period was quite simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the workflow already lives entirely inside Google Workspace, GAS is usually enough.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There was no need to provision infrastructure, Google handled the underlying platform, and future maintenance would be easier for whoever inherited the system next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating Intake and Registration Workflows
&lt;/h2&gt;

&lt;p&gt;By May 2025, the focus began to shift from automating tasks that I personally handled to building tools that could be used across the organization.&lt;/p&gt;

&lt;p&gt;One example was improving visibility into inquiries.&lt;/p&gt;

&lt;p&gt;When someone submitted a Google Form, the system would automatically generate a ticket number, copy the submission into a logging spreadsheet, send an acknowledgment email, and post a notification to Google Chat. Later, I added a simple web interface that allowed staff to track and update the status of each ticket.&lt;/p&gt;

&lt;p&gt;It was not a full-fledged support platform by any means, but I wanted to provide three capabilities that were essential for PTA operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirmation that an inquiry had been received&lt;/li&gt;
&lt;li&gt;The ability to track requests without losing anything&lt;/li&gt;
&lt;li&gt;Shared visibility among multiple volunteers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The volume of inquiries was not large enough to justify introducing an external SaaS solution, but relying solely on a shared email inbox was becoming difficult. This system occupied the middle ground, using forms, spreadsheets, and notifications to provide lightweight process management.&lt;/p&gt;

&lt;p&gt;In practice, PTA inquiries are relatively infrequent. Each request tends to be small, making it tempting for a volunteer to handle it privately and move on. However, that creates a black box where nobody else knows who received the request, how it was handled, or whether it was ever resolved.&lt;/p&gt;

&lt;p&gt;At this stage, the goal was straightforward: establish a reliable foundation through ticket numbers, activity logs, and notifications so that inquiry handling became visible and traceable.&lt;/p&gt;

&lt;p&gt;I described the implementation details of this system in a separate article:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yo-shi/diy-ticketing-system-with-google-apps-script-for-handling-inquiries-2ed4" class="crayons-story__hidden-navigation-link"&gt;DIY Ticketing System with Google Apps Script for Handling Inquiries&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yo-shi" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png" alt="yo-shi profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yo-shi" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Yoshi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Yoshi
                
              
              &lt;div id="story-author-preview-content-2511318" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yo-shi" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Yoshi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yo-shi/diy-ticketing-system-with-google-apps-script-for-handling-inquiries-2ed4" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 21 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yo-shi/diy-ticketing-system-with-google-apps-script-for-handling-inquiries-2ed4" id="article-link-2511318"&gt;
          DIY Ticketing System with Google Apps Script for Handling Inquiries
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gas"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gas&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tooling"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tooling&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/yo-shi/diy-ticketing-system-with-google-apps-script-for-handling-inquiries-2ed4" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/yo-shi/diy-ticketing-system-with-google-apps-script-for-handling-inquiries-2ed4#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              7&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Around the same time, I also released an automated mailing list registration system.&lt;/p&gt;

&lt;p&gt;The workflow took responses submitted through a form, added members to Google Groups, sent a confirmation email, and updated the processing status in a spreadsheet. As shown in the meeting document below, the previous process had surprisingly little verification. If someone requested a change, their information would often be updated without any confirmation of identity. The new system was far from perfect, but it was certainly an improvement. Looking back at the implementation, it appears I also included basic validation to check whether the requester and the registration email address matched.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpei5mcshkkvj3lryzyl9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpei5mcshkkvj3lryzyl9.png" alt="mail"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another change that proved valuable during this period was the introduction of Google Chat.&lt;/p&gt;

&lt;p&gt;The PTA had a deeply ingrained email culture. People would send messages like "Understood, thank you!" and include everyone on CC, resulting in overflowing inboxes filled with low-value email traffic. This was the point at which we started encouraging a simple rule: internal communication should happen in chat whenever possible.&lt;/p&gt;

&lt;p&gt;That cultural shift turned out to be important later. Once Google Chat became part of daily operations, building notification bots and automated alerts became much easier because the communication channel was already in place.&lt;/p&gt;

&lt;p&gt;Perhaps the most important lesson during this phase was learning to separate tasks that require human judgment from tasks that can safely be delegated to software.&lt;/p&gt;

&lt;p&gt;The content of an inquiry still requires a human to interpret and respond. However, assigning ticket numbers, sending acknowledgment emails, posting Chat notifications, and recording status updates can all be automated.&lt;/p&gt;

&lt;p&gt;The same principle applied to mailing list registration. Exceptional cases still required human review, but receiving requests, returning processing results, and maintaining logs could be handled automatically.&lt;/p&gt;

&lt;p&gt;I found myself constantly asking the same question:&lt;/p&gt;

&lt;p&gt;"What kinds of exceptions can occur, and can the system handle them automatically?"&lt;/p&gt;

&lt;p&gt;Thinking through those edge cases again and again turned out to be one of the most valuable experiences of the entire project. It shifted my focus from simply automating routine tasks to designing workflows that could remain reliable even when real-world operations became messy.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Became Clear That Forms and Email Alone Were Not Enough for Directory Management
&lt;/h2&gt;

&lt;p&gt;The development of the PTA directory registration system from June 2025 onward was one of the biggest turning points of the entire year.&lt;/p&gt;

&lt;p&gt;For the first time, the focus shifted from building standalone automation tools to designing an end-to-end process: how parent information should be collected, updated, and ultimately reflected in communication channels and operational systems.&lt;/p&gt;

&lt;p&gt;Before this redesign, the workflow was surprisingly fragmented. Student data changes received from the school were sent to an external vendor, which updated its own database and returned the updated records as Excel files. Mailing list updates were then performed manually using those spreadsheets.&lt;/p&gt;

&lt;p&gt;While this approach technically worked, it came with several problems. Updates were slow, duplicate records accumulated, outdated information remained in circulation, and the operational burden on volunteers was significant. In particular, even when a new parent registration or contact information change was submitted, the update would not reach the mailing lists until the next monthly processing cycle. In the worst case, a parent could go more than a month without receiving important communications.&lt;/p&gt;

&lt;p&gt;Frankly, that raised an obvious question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you really call it a directory system if updates take over a month to become effective?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was also one of the major sources of parent inquiries.&lt;/p&gt;

&lt;p&gt;This was the point where I finally introduced what I had wanted from the beginning: a proper database.&lt;/p&gt;

&lt;p&gt;The core directory management workflow was moved into BigQuery. Google Forms and Google Apps Script continued to handle data entry, but normalization, change detection, historical tracking, and mailing list eligibility calculations were all moved into BigQuery.&lt;/p&gt;

&lt;p&gt;For new registrations, GAS performed validation checks such as UUID verification, email address matching, and confirming that identity verification had occurred within a fifteen-minute window. Rather than writing directly into the production directory, submissions were first accepted as validated data.&lt;/p&gt;

&lt;p&gt;From there, BigQuery stored procedures updated tables such as &lt;code&gt;students&lt;/code&gt;, &lt;code&gt;guardians&lt;/code&gt;, and &lt;code&gt;ml_member_current&lt;/code&gt;, with the final step synchronizing eligible members to Google Groups.&lt;/p&gt;

&lt;p&gt;One of the most important components introduced during this transition was the mailing list uploader.&lt;/p&gt;

&lt;p&gt;This was the first time I was able to automate Google Groups management itself.&lt;/p&gt;

&lt;p&gt;Saving form responses and sending notifications had always been possible with GAS, but automating the addition and removal of group members required Google Workspace administrative privileges. Once those permissions were incorporated into the workflow, the system moved beyond merely automating record-keeping and into automating actual administrative operations.&lt;/p&gt;

&lt;p&gt;For the first time, the entire process—from registration to mailing list synchronization—became a single connected workflow.&lt;/p&gt;

&lt;p&gt;As a result, registrations could be accepted immediately, and mailing list updates could typically be completed the next day. A process that previously had a lead time of up to a month was reduced to roughly one day.&lt;/p&gt;

&lt;p&gt;That was the point where the system finally began to feel like a real system rather than a collection of helper scripts.&lt;/p&gt;

&lt;p&gt;The reasons for adopting BigQuery were equally straightforward.&lt;/p&gt;

&lt;p&gt;Spreadsheets alone had reached their limits for managing change detection and historical records. Comparing student data received from the school against form submissions, recalculating enrollment status and validity flags, and deriving mailing list membership from those results are all tasks that are dramatically easier to understand and maintain in SQL.&lt;/p&gt;

&lt;p&gt;Equally important, BigQuery integrates naturally with the rest of the Google ecosystem. It connected smoothly with GAS, Google Forms, Google Cloud Storage, and Google Groups, making it a particularly practical foundation for this type of operational workflow.&lt;/p&gt;

&lt;p&gt;Even within the Git repository, I established naming conventions such as &lt;code&gt;load_&lt;/code&gt;, &lt;code&gt;refresh_&lt;/code&gt;, and &lt;code&gt;update_&lt;/code&gt; to distinguish between ingestion, recalculation, and synchronization processes. This was not driven by technical necessity so much as maintainability. Months later, anyone looking at the code should be able to understand what a process does simply by reading its name.&lt;/p&gt;

&lt;p&gt;That may seem like a small detail, but it reflects one of the lessons I appreciated most from my professional experience working with data platforms and operational databases.&lt;/p&gt;

&lt;p&gt;Another important consideration was how student data was received from the school.&lt;/p&gt;

&lt;p&gt;From a purely technical perspective, an API integration would have been ideal. In reality, however, the school's existing workflow revolved around email, and changing that process would have created unnecessary friction for everyone involved.&lt;/p&gt;

&lt;p&gt;Instead, I designed the system around the current operational reality. At the beginning of each month, a scheduled email request is sent automatically. The school's reply email contains an attachment, which is stored in Google Cloud Storage. The latest file is then picked up by the BigQuery pipeline and processed automatically.&lt;/p&gt;

&lt;p&gt;It is not the most technically elegant architecture.&lt;/p&gt;

&lt;p&gt;However, it is an architecture that people can actually continue using.&lt;/p&gt;

&lt;p&gt;Throughout this project, that consideration consistently outweighed technical perfection.&lt;/p&gt;

&lt;p&gt;These changes transformed a monthly process that depended on volunteers sacrificing personal time to perform repetitive manual updates into one where data pipelines handle most of the work and people primarily review the results.&lt;/p&gt;

&lt;p&gt;New registrations, contact information changes, officer assignments, and student data updates all enter the system through different paths, but ultimately converge into the same BigQuery update pipeline. From there, updates flow outward to mailing lists, directory applications, and other downstream systems.&lt;/p&gt;

&lt;p&gt;Looking back, one design principle made the entire system sustainable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Many entry points, but a single source of truth.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Establishing that architecture became the foundation that allowed directory management to continue evolving throughout the year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Information Accessible Across the Organization
&lt;/h2&gt;

&lt;p&gt;Once the directory system was in place, the next challenge was making the information usable.&lt;/p&gt;

&lt;p&gt;In June 2025, I released a web-based PTA Directory Viewer. The architecture was intentionally simple: the latest data was periodically exported from BigQuery views into a spreadsheet, and the cached data was then displayed through a GAS web application.&lt;/p&gt;

&lt;p&gt;From a technical standpoint, there was nothing particularly sophisticated about the solution. One small but important design decision was implementing role-based access control. Board members could view the entire directory, transportation coordinators could see only the bus routes they managed, and class representatives could access information for their own classes. Row-level filtering and column restrictions were applied based on account identity and assigned roles.&lt;/p&gt;

&lt;p&gt;Directories are useful, but they also become a risk if too much information is exposed. From the beginning, the principle was straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show only the data that each person actually needs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Although it would have been possible to connect directly to BigQuery using something like Connected Sheets, I intentionally chose a scheduled export and cache-based architecture. The reasons were simple: predictable performance, stable costs, and operational simplicity.&lt;/p&gt;

&lt;p&gt;A directory viewer should be lightweight, reliable, and easy to explain. Unless real-time updates are essential, a cached approach is often much easier to operate.&lt;/p&gt;

&lt;p&gt;An equally important challenge was user education.&lt;/p&gt;

&lt;p&gt;Before this system existed, individual departments would download copies of the directory spreadsheet and maintain their own versions. As a result, nobody knew which file contained the latest information. Contact changes were sometimes communicated through unofficial channels without the directory administrator even knowing about them.&lt;/p&gt;

&lt;p&gt;I vividly remember spending a significant amount of time encouraging people to abandon the familiar copy-and-paste workflow and use the directory viewer as the single source of truth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2d8bg9wdbhkft2d606un.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2d8bg9wdbhkft2d606un.png" alt="Stop"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Following the same philosophy, I introduced class-specific mailing lists for class representatives in September 2025.&lt;/p&gt;

&lt;p&gt;Groups were created on a per-class, per-school-year basis, and memberships were synchronized automatically from the master directory. The key difference was that synchronization was based on the desired end state rather than simply adding new members. Enrollment and withdrawal status were continuously evaluated, and each mailing list was updated to reflect the current roster.&lt;/p&gt;

&lt;p&gt;This marked another shift in thinking. Rather than treating mailing list management as an isolated administrative task, the communication infrastructure became something generated directly from the directory platform itself.&lt;/p&gt;

&lt;p&gt;One immediate benefit was that class representatives no longer had to manually maintain long BCC lists every time they sent an email.&lt;/p&gt;

&lt;p&gt;Around the same period, we also revisited the PTA website.&lt;/p&gt;

&lt;p&gt;Historically, the organization had rented a server and maintained a WordPress installation. When we evaluated the actual communication needs of the PTA, however, the setup was clearly overengineered.&lt;/p&gt;

&lt;p&gt;Fortunately, our communications team raised the issue as well, and we eventually migrated the site to Google Sites. This reduced both ongoing costs and operational complexity.&lt;/p&gt;

&lt;p&gt;Once again, the primary decision criterion was not feature richness.&lt;/p&gt;

&lt;p&gt;It was sustainability.&lt;/p&gt;

&lt;p&gt;Could the next volunteer maintain it without specialized technical knowledge?&lt;/p&gt;

&lt;p&gt;That question mattered far more than whether the platform supported advanced functionality.&lt;/p&gt;

&lt;p&gt;To be honest, Google Sites lacks the flexibility of something like a static site hosted on Amazon S3. Content updates are largely performed through the graphical interface rather than code. However, when considering future maintenance, the tradeoff was worthwhile. Updating the site became something that even volunteers with no IT background could realistically manage.&lt;/p&gt;

&lt;p&gt;As part of the website refresh, I also integrated Google Analytics and created a chatbot that automatically posted weekly traffic reports to Google Chat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gfrd50cjcm1fta478qc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gfrd50cjcm1fta478qc.png" alt="bot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During the same period, I revisited another unique PTA process: cultural resource lending.&lt;/p&gt;

&lt;p&gt;The PTA maintains a collection of several hundred educational and cultural materials that members can borrow. Previously, users had to download an Excel file, fill in the items they wanted to borrow, and then send the file via email. The process involved multiple steps and frequently resulted in incomplete submissions.&lt;/p&gt;

&lt;p&gt;For reasons nobody could fully explain, requests were also being sent to a custom email address that only existed for this purpose.&lt;/p&gt;

&lt;p&gt;The workflow was redesigned around a Google Form. Users now enter the required information once, and the system automatically generates and sends a loan request spreadsheet with the submitted information pre-filled.&lt;/p&gt;

&lt;p&gt;The result was a simpler experience for users and a more consistent format for administrators receiving requests. By standardizing the process, we reduced procedural friction on both sides.&lt;/p&gt;

&lt;p&gt;Another major change during this period was adopting local development with &lt;code&gt;clasp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When everything lives exclusively inside Google Apps Script, it becomes difficult to track what changed, when it changed, and why it changed. Moving the code into a Git repository allowed me to manage changes through commits and preserve the intent behind each modification.&lt;/p&gt;

&lt;p&gt;That was the point where the project started to feel less like a collection of utility scripts and more like a maintainable software project.&lt;/p&gt;

&lt;p&gt;In hindsight, I honestly have no idea how I managed to develop GAS applications for so long without &lt;code&gt;clasp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you are building anything substantial with Google Apps Script, I strongly recommend using it from the beginning.&lt;/p&gt;

&lt;p&gt;For anyone completely new to &lt;code&gt;clasp&lt;/code&gt;, I previously wrote about my own setup struggles in the article:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yo-shi/my-experience-fixing-clasp-login-errors-on-google-workspace-3nh" class="crayons-story__hidden-navigation-link"&gt;My Experience Fixing clasp Login Errors on Google Workspace&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yo-shi" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png" alt="yo-shi profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yo-shi" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Yoshi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Yoshi
                
              
              &lt;div id="story-author-preview-content-2826676" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yo-shi" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Yoshi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yo-shi/my-experience-fixing-clasp-login-errors-on-google-workspace-3nh" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 7 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yo-shi/my-experience-fixing-clasp-login-errors-on-google-workspace-3nh" id="article-link-2826676"&gt;
          My Experience Fixing clasp Login Errors on Google Workspace
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gas"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gas&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/yo-shi/my-experience-fixing-clasp-login-errors-on-google-workspace-3nh" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/yo-shi/my-experience-fixing-clasp-login-errors-on-google-workspace-3nh#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;h2&gt;
  
  
  The Hardest Challenge: Building a Payment System
&lt;/h2&gt;

&lt;p&gt;From late September through October 2025, I built the PTA membership fee payment platform.&lt;/p&gt;

&lt;p&gt;Of all the projects completed during the year, this was the one that required the most thought about system design rather than simple automation.&lt;/p&gt;

&lt;p&gt;The initial implementation was straightforward. Billing records were stored in BigQuery, while Google Apps Script managed the association between invoices and Stripe Checkout URLs. On paper, the design seemed reasonable.&lt;/p&gt;

&lt;p&gt;In practice, it failed almost immediately.&lt;/p&gt;

&lt;p&gt;The problem was that pre-generated &lt;code&gt;checkout_url&lt;/code&gt; values and &lt;code&gt;session_id&lt;/code&gt;s eventually expire. Parents would sometimes open billing emails days or weeks later, only to discover that the payment link was no longer valid.&lt;/p&gt;

&lt;p&gt;That realization forced a major redesign.&lt;/p&gt;

&lt;p&gt;Instead of embedding Stripe URLs directly in emails, I switched to a model where emails contained only PTA-issued signed links. When a user clicked the link, the server would generate a fresh Stripe Checkout Session on demand and immediately redirect the user to Stripe.&lt;/p&gt;

&lt;p&gt;Payment completion was treated as a separate source of truth. Rather than trusting redirects or browser behavior, the system relied on Stripe Webhooks. When a payment event arrived, the webhook updated the corresponding &lt;code&gt;payment_status&lt;/code&gt; and &lt;code&gt;paid_at&lt;/code&gt; fields in BigQuery.&lt;/p&gt;

&lt;p&gt;In addition, every generated session, webhook event, and outbound transaction log was stored in separate tables to create a complete audit trail.&lt;/p&gt;

&lt;p&gt;The resulting architecture looked much closer to a real payment platform than the simple integration I had originally envisioned.&lt;/p&gt;

&lt;p&gt;I documented the implementation details in a separate article:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yo-shi/how-i-automated-membership-fee-collection-using-stripe-and-google-services-2gd4" class="crayons-story__hidden-navigation-link"&gt;How I Automated Membership Fee Collection Using Stripe and Google Services&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yo-shi" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png" alt="yo-shi profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yo-shi" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Yoshi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Yoshi
                
              
              &lt;div id="story-author-preview-content-2938567" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yo-shi" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Yoshi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yo-shi/how-i-automated-membership-fee-collection-using-stripe-and-google-services-2gd4" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 19 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yo-shi/how-i-automated-membership-fee-collection-using-stripe-and-google-services-2gd4" id="article-link-2938567"&gt;
          How I Automated Membership Fee Collection Using Stripe and Google Services
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/stripe"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;stripe&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/googlecloud"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;googlecloud&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/yo-shi/how-i-automated-membership-fee-collection-using-stripe-and-google-services-2gd4" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/yo-shi/how-i-automated-membership-fee-collection-using-stripe-and-google-services-2gd4#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;This was also the first major component where I intentionally moved away from GAS and adopted Cloud Run with Python.&lt;/p&gt;

&lt;p&gt;The reason was simple: I had reached the practical limits of Google Apps Script.&lt;/p&gt;

&lt;p&gt;Webhook signature verification, raw request body handling, secure secret management through Secret Manager, on-demand session generation, and idempotency controls are all technically possible with GAS if you work hard enough. However, once security, maintainability, and operational reliability become priorities, a server-side application is a much more natural fit.&lt;/p&gt;

&lt;p&gt;One lesson from this project was learning where each technology belongs.&lt;/p&gt;

&lt;p&gt;GAS is excellent for workflow automation inside Google Workspace.&lt;/p&gt;

&lt;p&gt;Cloud Run is a better fit when security-sensitive integrations and external payment systems become the core of the application.&lt;/p&gt;

&lt;p&gt;The payment system became an exercise in drawing that boundary clearly.&lt;/p&gt;

&lt;p&gt;Around the payment platform, I also built supporting operational tools. These included email workflows for initial billing notifications, payment reminders, and escalation alerts for unpaid balances. I previously wrote about some of the challenges involved in those email processes in another article:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/yo-shi/lets-stick-to-gmailapp-over-mailapp-in-google-apps-script-d2i" class="crayons-story__hidden-navigation-link"&gt;Let's Stick to GmailApp Over MailApp in Google Apps Script&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/yo-shi" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png" alt="yo-shi profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yo-shi" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Yoshi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Yoshi
                
              
              &lt;div id="story-author-preview-content-3010000" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yo-shi" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2386099%2F3cfc456b-d3cb-4e85-8e2f-3538fc60b5e5.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Yoshi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/yo-shi/lets-stick-to-gmailapp-over-mailapp-in-google-apps-script-d2i" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 10 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/yo-shi/lets-stick-to-gmailapp-over-mailapp-in-google-apps-script-d2i" id="article-link-3010000"&gt;
          Let's Stick to GmailApp Over MailApp in Google Apps Script
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gas"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gas&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/learning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;learning&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/yo-shi/lets-stick-to-gmailapp-over-mailapp-in-google-apps-script-d2i#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;All outbound email activity was logged into BigQuery as well.&lt;/p&gt;

&lt;p&gt;A payment system is not simply about whether someone can successfully pay. Just as important is being able to answer operational questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who received which email?&lt;/li&gt;
&lt;li&gt;When was it sent?&lt;/li&gt;
&lt;li&gt;Which step of the process is currently blocked?&lt;/li&gt;
&lt;li&gt;What evidence exists if someone claims they never received a notification?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of that, auditability became a first-class design requirement throughout the project.&lt;/p&gt;

&lt;p&gt;Looking back, this was probably the point where the system stopped being a collection of productivity tools and started resembling an actual business application. The technical challenge was not collecting money. It was building a process that could be trusted, monitored, and explained when something inevitably went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Helping Other Departments Become More Efficient Through Technology
&lt;/h2&gt;

&lt;p&gt;By November, most of the core directory and payment systems were in place. As the end of the school year approached, I shifted my attention toward building tools that supported day-to-day operational activities across the organization.&lt;/p&gt;

&lt;p&gt;One of the first areas targeted was the library volunteer program.&lt;/p&gt;

&lt;p&gt;Parents volunteer to help with book lending and returns, but each shift lasts four to five hours, making it a significant commitment. Every year, the PTA collected availability from a large number of parents and manually created schedules.&lt;/p&gt;

&lt;p&gt;On the surface, it looked like a simple scheduling task.&lt;/p&gt;

&lt;p&gt;In reality, the workload extended far beyond creating the initial schedule. Collecting preferences, assigning volunteers, handling cancellations, coordinating replacements, and communicating with the school all created a surprisingly complex operational process.&lt;/p&gt;

&lt;p&gt;Initially, I thought about creating something similar to a SignUpGenius-style volunteer registration form. However, the requirements turned out to be much more complicated than simply allowing people to sign up for open dates.&lt;/p&gt;

&lt;p&gt;Some slots needed to be assigned on a first-come, first-served basis. Others required lottery-based selection after the application deadline. The system also needed to handle absences, replacement requests, and notifications to school staff. In short, there were many organization-specific rules that had accumulated over the years.&lt;/p&gt;

&lt;p&gt;As a result, beginning in February 2026, I developed a dedicated library volunteer platform using Google Apps Script. The system handled volunteer registration, assignment processing, replacement requests, and notification logging as part of a single workflow.&lt;/p&gt;

&lt;p&gt;Part of the user manual is shown below. Unfortunately, the manual is available only in Japanese.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphn5hdem3aur1gmjh3ce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphn5hdem3aur1gmjh3ce.png" alt="user manual"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The platform was designed so that most day-to-day operations could be managed directly by the users themselves. Rather than relying on administrators to coordinate every step, participants could view their status, understand what actions were required, and manage their own involvement.&lt;/p&gt;

&lt;p&gt;In that sense, it evolved beyond a simple administrative tool and became something closer to a self-service operational platform.&lt;/p&gt;

&lt;p&gt;The system is already in production and currently supports the full process from volunteer recruitment through assignment and replacement management.&lt;/p&gt;

&lt;p&gt;Around the same time, preparations began for selecting the following year's PTA officers.&lt;/p&gt;

&lt;p&gt;Officer selection is far more complicated than it might initially appear. Eligibility and selection probabilities vary significantly based on bylaws, internal policies, prior service history, and numerous other conditions.&lt;/p&gt;

&lt;p&gt;Historically, the selection process relied on externally generated spreadsheets, extensive manual filtering, email distribution, and follow-up phone calls to confirm responses. Every year, a considerable amount of effort went into producing and managing candidate lists.&lt;/p&gt;

&lt;p&gt;I automated the entire workflow.&lt;/p&gt;

&lt;p&gt;Candidate selection, email notifications, acceptance confirmations, and tracking were all integrated into a single process.&lt;/p&gt;

&lt;p&gt;A task that previously consumed nearly a month of work, involving multiple volunteers and significant administrative overhead, could now be executed with essentially a single button press once the underlying rules were configured.&lt;/p&gt;

&lt;p&gt;At the same time, I added a generalized email distribution platform.&lt;/p&gt;

&lt;p&gt;For each project, administrators can prepare a data sheet containing the target audience. The system joins that data against the central family master using &lt;code&gt;unique_id&lt;/code&gt;, then merges personalized information into email templates before sending messages.&lt;/p&gt;

&lt;p&gt;This tool was only possible because several foundational pieces were already in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each family could be uniquely identified through &lt;code&gt;unique_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Contact information had been centralized and maintained&lt;/li&gt;
&lt;li&gt;Target populations could be generated from the directory system as structured datasets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because those prerequisites already existed, the email distribution platform itself was surprisingly quick to build.&lt;/p&gt;

&lt;p&gt;Looking back, that was perhaps the most satisfying aspect of the entire year.&lt;/p&gt;

&lt;p&gt;Each improvement made the next improvement easier.&lt;/p&gt;

&lt;p&gt;The directory platform enabled mailing list automation. The mailing list infrastructure enabled communication tools. The communication tools enabled volunteer management and officer selection. The centralized data model enabled personalized outreach.&lt;/p&gt;

&lt;p&gt;In other words, every project became a building block for the next one.&lt;/p&gt;

&lt;p&gt;Viewed from that perspective, the email distribution platform was not just another tool. It was the culmination of a year's worth of accumulated infrastructure, process improvements, and architectural decisions finally coming together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technology Decisions Were Driven Not by Trendiness, but by What the PTA Could Sustain
&lt;/h2&gt;

&lt;p&gt;One thing I found myself thinking about repeatedly throughout the year was not whether a technology was good, but whether it was a reasonable fit for the organization.&lt;/p&gt;

&lt;p&gt;In a typical product development environment, priorities might center around scalability, separation of concerns, reusability, type safety, deployment strategies, and other architectural considerations. Within a PTA, however, different constraints mattered more. The system needed to be manageable by a small group of volunteers, affordable to operate, easy to hand over to future administrators, and compatible with the existing Google Workspace environment.&lt;/p&gt;

&lt;p&gt;Those requirements naturally led to extensive use of Google Apps Script.&lt;/p&gt;

&lt;p&gt;GAS integrates closely with Forms, Sheets, Gmail, Drive, and Google Chat, allowing entire workflows to be implemented with relatively little code. Personally, I often find it more approachable than AWS Lambda for this type of operational automation.&lt;/p&gt;

&lt;p&gt;That said, GAS is not the right solution for everything.&lt;/p&gt;

&lt;p&gt;Complex change detection, historical data management, and cross-system data analysis are much better handled by BigQuery. Likewise, systems involving payment processing, security boundaries, and webhook-driven integrations are generally more reliable when implemented on Cloud Run.&lt;/p&gt;

&lt;p&gt;The key was understanding where each tool fit best.&lt;/p&gt;

&lt;p&gt;At least in my experience, staying within the Google ecosystem wherever possible reduces both implementation effort and long-term maintenance costs. The tools work well together, and the learning curve remains manageable for future volunteers.&lt;/p&gt;

&lt;p&gt;Cost management was another factor that could not be ignored.&lt;/p&gt;

&lt;p&gt;The decision to migrate from WordPress to Google Sites came from the same line of thinking. Likewise, whenever a solution could be implemented using existing Google Workspace services, GAS, or BigQuery, I generally preferred to keep it there rather than introduce additional platforms.&lt;/p&gt;

&lt;p&gt;For a PTA, the most important question is not whether something is cost-effective today.&lt;/p&gt;

&lt;p&gt;It is whether the solution will still be practical and maintainable next year.&lt;/p&gt;

&lt;p&gt;A system that saves time but introduces significant hosting costs, licensing fees, or maintenance overhead often fails to survive leadership transitions.&lt;/p&gt;

&lt;p&gt;In that sense, technology selection was really a form of operational design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Systems That Can Survive Leadership Turnover
&lt;/h2&gt;

&lt;p&gt;The idea of handover was always in the back of my mind throughout this project.&lt;/p&gt;

&lt;p&gt;In organizations where the same people maintain systems for many years, it is sometimes acceptable for knowledge to remain concentrated in a few individuals. As long as those people understand the system, operations continue.&lt;/p&gt;

&lt;p&gt;PTAs do not work that way.&lt;/p&gt;

&lt;p&gt;A completely different person may inherit responsibility a year later. In some cases, the handover process begins only a few months after a system is introduced.&lt;/p&gt;

&lt;p&gt;Because of that, I tried to avoid creating solutions that required specialized administrative interfaces or extensive training.&lt;/p&gt;

&lt;p&gt;Most of the systems were intentionally built around tools people already knew: Google Forms, Google Sheets, email, and Google Chat.&lt;/p&gt;

&lt;p&gt;Rather than asking every new volunteer to learn a sophisticated custom application, I preferred to extend familiar tools and workflows whenever possible.&lt;/p&gt;

&lt;p&gt;However, an even more important lesson emerged over time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before automating a process, ask whether the process should exist at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As these systems were introduced, many responsibilities within the PTA gradually disappeared. In several cases, entire positions became unnecessary because the work they were created to perform no longer existed.&lt;/p&gt;

&lt;p&gt;One example was the class representative role.&lt;/p&gt;

&lt;p&gt;A major responsibility of class representatives had been collecting PTA membership fees. Across all grades, from preschool through high school, this involved roughly eighty volunteers. Once fee collection became automated, the need for that responsibility disappeared. As a consequence, several associated administrative tasks—including Google account management responsibilities assigned to those positions—also disappeared.&lt;/p&gt;

&lt;p&gt;Ironically, that meant the original "Account Administrator" role I had volunteered for effectively eliminated itself.&lt;/p&gt;

&lt;p&gt;Looking ahead, the biggest challenge is ensuring that the system remains maintainable.&lt;/p&gt;

&lt;p&gt;Even during the past year, the amount of time I spent writing entirely new code gradually decreased. More and more of the work shifted toward organizing requirements, making decisions, and coordinating improvements rather than implementation itself.&lt;/p&gt;

&lt;p&gt;My current area of interest is exploring whether future maintenance can be handled differently.&lt;/p&gt;

&lt;p&gt;When bug reports or feature requests arrive, perhaps humans only need to decide whether the request is worthwhile. The implementation details, troubleshooting, documentation updates, and even user support responses might eventually be delegated to AI agents.&lt;/p&gt;

&lt;p&gt;Whether that vision proves practical remains to be seen.&lt;/p&gt;

&lt;p&gt;But after spending a year automating administrative work, it seems only natural to start asking how much of the software maintenance process itself can also be automated.&lt;/p&gt;

</description>
      <category>gas</category>
      <category>beginners</category>
      <category>automation</category>
    </item>
    <item>
      <title>Understanding Automata Theory Through Route Maps</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Sun, 28 Dec 2025 04:35:23 +0000</pubDate>
      <link>https://dev.to/yo-shi/lets-stick-to-gmailapp-over-mailapp-in-google-apps-script-f63</link>
      <guid>https://dev.to/yo-shi/lets-stick-to-gmailapp-over-mailapp-in-google-apps-script-f63</guid>
      <description>&lt;p&gt;Formal verification is a technique used to guarantee that software behaves correctly. In formal verification, the behavior of a system is modeled mathematically, and the specifications it must satisfy are expressed as logical formulas.&lt;/p&gt;

&lt;p&gt;Rather than simply listing states, the goals are to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Represent specifications as mathematical models&lt;/li&gt;
&lt;li&gt;Mechanically determine whether those specifications are always satisfied or violated&lt;/li&gt;
&lt;li&gt;Guarantee safety, including cases that are easy to miss with testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Automata theory is used as a tool to achieve these goals.&lt;/p&gt;

&lt;p&gt;In this field, many technical terms such as DFA, NFA, NBA, and LTL appear one after another and can be confusing. Abstract concepts like “state transitions,” “nondeterminism,” and “infinite executions” did not make much sense to me at first. However, my understanding improved by visualizing these ideas using a train route map as an analogy.&lt;/p&gt;

&lt;p&gt;By using the familiar image of a route map, I believe it becomes easier to develop an intuitive understanding of how formal verification works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intended Audience
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;People who have just started learning automata theory or formal verification at university&lt;/li&gt;
&lt;li&gt;People who feel “I kind of understand it, but it doesn’t really click” in classes on model checking or LTL&lt;/li&gt;
&lt;li&gt;Engineers who are interested in system verification&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two Axes for Classifying Automata
&lt;/h2&gt;

&lt;p&gt;When trying to understand automata, there are actually only two viewpoints you need to keep in mind.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Whether the behavior is deterministic or nondeterministic&lt;/li&gt;
&lt;li&gt;Whether the execution is finite or infinite&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By combining these two axes, automata can theoretically be classified into four types.&lt;br&gt;
Abbreviations such as DFA, NFA, DBA, and NBA represent the differences in these combinations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Deterministic × Finite&lt;br&gt;
DFA (Deterministic Finite Automaton)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nondeterministic × Finite&lt;br&gt;
NFA (Nondeterministic Finite Automaton)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deterministic × Infinite&lt;br&gt;
DBA (Deterministic Büchi Automaton)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nondeterministic × Infinite&lt;br&gt;
NBA (Nondeterministic Büchi Automaton)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What determinism and nondeterminism mean, and what infinite execution refers to, will be explained later with concrete examples. In addition, DBA, which handles deterministic infinite executions, will be omitted in this explanation from the perspective of expressiveness and practical usefulness.&lt;/p&gt;

&lt;p&gt;With that said, let us start by looking at the simplest and most intuitive case: DFA (deterministic, finite execution).&lt;/p&gt;
&lt;h2&gt;
  
  
  Start with the Simplest DFA: A Single-Line Route
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What Is a DFA (Deterministic Finite Automaton)?
&lt;/h3&gt;

&lt;p&gt;A DFA (Deterministic Finite Automaton) is the simplest type of automaton. In terms of a route map, it corresponds to a single-line route.&lt;/p&gt;

&lt;p&gt;For example, consider the following route.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdbd4rlq9rd4y51s17xe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdbd4rlq9rd4y51s17xe.png" alt="dfa" width="387" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This route has the following characteristics.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stations (states): Start, G0, Goal&lt;/li&gt;
&lt;li&gt;Tracks (transitions): connections from each station to the next&lt;/li&gt;
&lt;li&gt;Ticket (input): the action “take the Green Line”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important point is that &lt;strong&gt;when you are at a given station, there is always exactly one next station you can move to&lt;/strong&gt;. If you take the Green Line from the Start station, you will always arrive at station G1. There are no other choices.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mathematical Definition
&lt;/h3&gt;

&lt;p&gt;Formally, a DFA is defined as the following tuple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A = (Q, q₀, Σ, δ, F)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Q: a set of states (the set of stations)&lt;/li&gt;
&lt;li&gt;q₀: the initial state (the starting station)&lt;/li&gt;
&lt;li&gt;Σ: a set of input symbols (types of routes)&lt;/li&gt;
&lt;li&gt;δ: the transition function (which station you can go to from which station)&lt;/li&gt;
&lt;li&gt;F: a set of accepting states (destination stations)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mathematical definition can be visualized as a state transition diagram. In practice and in introductory textbooks, this diagram itself is often referred to as “the automaton.” Strictly speaking, an automaton is a single definition that combines the set of states, transitions, initial state, and acceptance condition. If there are multiple such definitions, they are called automata (the plural form).&lt;/p&gt;

&lt;h2&gt;
  
  
  Extension to NFA: A Multi-Track Route
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Is an NFA (Nondeterministic Finite Automaton)?
&lt;/h3&gt;

&lt;p&gt;An NFA is an extension of a DFA. The “D” in DFA stands for “Deterministic,” which changes to “Nondeterministic” in an NFA. In other words, there can be multiple possible next states. In terms of a route map, this corresponds to a multi-track route.&lt;/p&gt;

&lt;p&gt;For example, consider a case where there are multiple ways to go from the Start station to the Goal station.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbabvq92ca5gx84fgczu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbabvq92ca5gx84fgczu7.png" alt="nfa" width="378" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this route network, starting from the same Start station, the path can branch into multiple routes under the same condition. From G1, you can go directly to Goal via the Green Line, or you can go via B1 on the Blue Line. From B1 as well, there are multiple choices: going through G1 or going directly to Goal.&lt;/p&gt;

&lt;p&gt;This is what “nondeterminism” means. Which route is taken is not determined at that moment. However, the key idea of an NFA is that &lt;strong&gt;it is acceptable as long as there exists at least one route that reaches the goal&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extension to NBA: Circular Lines and Infinite Time
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Is an NBA (Nondeterministic Büchi Automaton)?
&lt;/h3&gt;

&lt;p&gt;An NBA is a further extension of an NFA. In terms of a route map, it corresponds to a network that includes circular lines.&lt;/p&gt;

&lt;p&gt;For example, consider the following circular route, the Circle Line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4rnnngmph4ro4p6qkxcy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4rnnngmph4ro4p6qkxcy.png" alt="nba" width="393" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key feature of this route is that &lt;strong&gt;you can keep going around it indefinitely&lt;/strong&gt;. The train can continue running forever.&lt;/p&gt;

&lt;p&gt;An NBA deals with exactly this kind of &lt;strong&gt;infinite behavior&lt;/strong&gt;. Programs and systems often “run forever once they are started.” To reason about such infinite executions, NBA is required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clearing Up a Common Misunderstanding: Time Is Infinite, States Are Finite
&lt;/h3&gt;

&lt;p&gt;There is a very important point here.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is infinite is the execution time; the number of states is finite.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fictional Circle Line has only 8 stations (a finite number), but you can loop around it indefinitely (infinite time). In the same way, an NBA represents infinite behavior using a finite number of states.&lt;/p&gt;

&lt;p&gt;As mentioned at the beginning, let us restate the key points for understanding automata.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Whether the behavior is deterministic or nondeterministic&lt;/li&gt;
&lt;li&gt;Whether the execution is finite or infinite&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When I first started learning automata, I mixed up points 1 and 2. As a result, I mistakenly thought that if the number of executions is infinite, then the behavior—that is, the outcomes of actions—must also have infinitely many patterns.&lt;/p&gt;

&lt;p&gt;In reality, even if there are multiple possible transitions, the number of states themselves remains finite once those transitions are properly grouped.&lt;/p&gt;

&lt;p&gt;In other words, what we are doing here is not “turning infinity into finiteness.”&lt;br&gt;
More precisely,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;we are making infinite-time behavior decidable using an automaton with a finite number of states&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Looking at a Concrete Example with a Traffic Light System
&lt;/h2&gt;

&lt;p&gt;Let us now walk through everything discussed so far using a single concrete example.&lt;/p&gt;
&lt;h3&gt;
  
  
  System: Traffic Lights at an Intersection
&lt;/h3&gt;

&lt;p&gt;A traffic light has three states.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 Red&lt;/li&gt;
&lt;li&gt;🟡 Yellow&lt;/li&gt;
&lt;li&gt;🟢 Green&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The state transitions are as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Green] --time passes--&amp;gt; [Yellow] --time passes--&amp;gt; [Red] --time passes--&amp;gt; [Green] --&amp;gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This forms a cycle and repeats infinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Representation as a DFA
&lt;/h3&gt;

&lt;p&gt;If we represent this traffic light as a DFA, the state at each moment is deterministically defined. If the current state is “Green,” the next state is always “Yellow.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Representation as an NBA
&lt;/h3&gt;

&lt;p&gt;Because the traffic light continues operating forever, it is natural to represent it as an NBA. The infinite loop “Green → Yellow → Red → Green → ...” can be expressed using just three states.&lt;/p&gt;

&lt;h2&gt;
  
  
  LTL Is a Language for Writing “Rules of Change”
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Is LTL (Linear Temporal Logic)?
&lt;/h3&gt;

&lt;p&gt;So far, we have described how a system behaves. The next step is to check whether the system is behaving correctly.&lt;/p&gt;

&lt;p&gt;For this purpose, LTL (Linear Temporal Logic) is used.&lt;/p&gt;

&lt;p&gt;LTL is a logic for writing specifications about time. Using the route map analogy, it describes “operating rules”; for traffic lights, it describes “rules about how colors change,” written using symbols.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Simple Explanation of LTL Operators
&lt;/h3&gt;

&lt;p&gt;LTL uses the following operators to write specifications precisely. This avoids ambiguities that can arise in natural language.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;□&lt;/code&gt; = &lt;code&gt;G&lt;/code&gt; (Globally): always&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;◇&lt;/code&gt; = &lt;code&gt;F&lt;/code&gt; (Finally): eventually&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;〇&lt;/code&gt; = &lt;code&gt;X&lt;/code&gt; (neXt): at the next moment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;U&lt;/code&gt; (Until): until&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that &lt;code&gt;Globally&lt;/code&gt; and &lt;code&gt;Finally&lt;/code&gt; are not primitive operators. They are derived from &lt;code&gt;U&lt;/code&gt; and negation &lt;code&gt;¬&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;G p ≡ ¬F ¬p&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F p ≡ true U p&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By combining these operators, complex temporal properties can be expressed.&lt;br&gt;
Let us now look at concrete examples using the traffic light system.&lt;/p&gt;

&lt;h4&gt;
  
  
  Specification 1: “After red, it must always become green”
&lt;/h4&gt;

&lt;p&gt;For a traffic light, this means: “Once red is on, green must turn on afterward.”&lt;/p&gt;

&lt;p&gt;In LTL: &lt;code&gt;G(Red → X Green)&lt;/code&gt; (green at the next moment)&lt;/p&gt;

&lt;h4&gt;
  
  
  Specification 2: “Green and red must not be on at the same time”
&lt;/h4&gt;

&lt;p&gt;For a traffic light, this means: “There must be no state where red and green are on simultaneously.”&lt;/p&gt;

&lt;p&gt;In LTL: &lt;code&gt;G ¬(Green ∧ Red)&lt;/code&gt; (always not (green and red))&lt;/p&gt;

&lt;h4&gt;
  
  
  Specification 3: “Green appears infinitely often”
&lt;/h4&gt;

&lt;p&gt;For a traffic light, this means: “No matter how much time passes, green should keep appearing repeatedly.”&lt;/p&gt;

&lt;p&gt;In LTL: &lt;code&gt;GF Green&lt;/code&gt; (always, eventually green)&lt;/p&gt;

&lt;h2&gt;
  
  
  From LTL to NBA
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Is the Conversion Necessary?
&lt;/h3&gt;

&lt;p&gt;LTL is &lt;strong&gt;a language that is easy for humans to write specifications in&lt;/strong&gt;. However, for a computer to check them, a more mechanically manageable form is required.&lt;/p&gt;

&lt;p&gt;That form is the NBA.&lt;/p&gt;

&lt;h3&gt;
  
  
  LTL Can Be Converted into an NBA
&lt;/h3&gt;

&lt;p&gt;In fact, it has been mathematically proven that any specification written in LTL can always be converted into a corresponding NBA. This follows from the property that LTL belongs to the class of ω-regular languages. ω-regular languages are an extension of regular languages that can handle infinite-length strings (infinite sequences of symbols), but we will omit the details here.&lt;/p&gt;

&lt;p&gt;For example, the specification “green appears infinitely often,” written as &lt;code&gt;GF Green&lt;/code&gt;, can be converted into the following NBA.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filv78alvhoyirtejl1l4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filv78alvhoyirtejl1l4.png" alt="ltl" width="424" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This NBA checks at S1, starting from Start, whether the current symbol is “Green” or “not Green.” If it is Green, the automaton transitions to S2, accepts the occurrence of Green, and then transitions back to S1 in the next step. If it is not Green, it loops back to S1. As a result, this NBA accepts exactly those runs that pass through Green infinitely often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the System Against the Specification
&lt;/h2&gt;

&lt;p&gt;So how do we actually check whether a system satisfies a given specification?&lt;/p&gt;

&lt;h3&gt;
  
  
  Overall Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Represent the actual system as a state transition diagram&lt;br&gt;&lt;br&gt;
Represent the traffic light system as an NBA: “Green → Yellow → Red → Green → ...”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Write the specification in LTL&lt;br&gt;&lt;br&gt;
Write “green appears infinitely often” as &lt;code&gt;GF Green&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Convert the specification into an NBA&lt;br&gt;&lt;br&gt;
Mechanically generate an NBA corresponding to &lt;code&gt;GF Green&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Take the product of the two NBAs&lt;br&gt;&lt;br&gt;
Combine the NBA of the actual system with the NBA of the specification&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check whether there exists a path that satisfies the condition&lt;br&gt;&lt;br&gt;
Search for an accepting infinite path in the product automaton&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In terms of the traffic light example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The actual behavior of the traffic light: Green → Yellow → Red → Green → ... (the system)
&lt;/li&gt;
&lt;li&gt;A “checking pattern” that represents the operational rule (the specification NBA)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By overlaying these two, we check &lt;strong&gt;whether there exists a behavior that does not violate the rule&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If a violating path is found, that path represents a “bug.” Formal verification tools (model checkers) present such a path as a counterexample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Value: Why Formal Verification Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bug detection

&lt;ul&gt;
&lt;li&gt;It enables exhaustive exploration of complex timing-dependent and rare-case bugs that are difficult to find with conventional testing.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Safety guarantees

&lt;ul&gt;
&lt;li&gt;In systems that affect human lives, such as aircraft or medical devices, it is necessary to mathematically prove the absence of bugs. Formal verification provides a powerful means to do so.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Clarification of specifications

&lt;ul&gt;
&lt;li&gt;The process of writing specifications in LTL helps clarify what the system is truly required to guarantee.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By using the route map analogy, the specialized terminology of automata theory becomes more intuitive. Although formal verification may appear difficult at first, its essence is simply to describe “how a system behaves” and “which rules it must satisfy” in mathematical terms, and then check them mechanically. Understanding that automata serve as the tools for this purpose makes the subject feel much more approachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://anushsom.medium.com/automata-theory-basics-for-dummies-by-a-dummy-a4f10d97314d" rel="noopener noreferrer"&gt;BASICS OF AUTOMATA THEORY FOR DUMMIES BY A DUMMY&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cds.caltech.edu/~murray/courses/eeci-sp12/L3_ltl-14May12.pdf" rel="noopener noreferrer"&gt;Automata-Based Representation of Linear-Time
Properties and Linear Temporal Logic (LTL)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>computerscience</category>
      <category>automaton</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Let's Stick to GmailApp Over MailApp in Google Apps Script</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Mon, 10 Nov 2025 15:07:49 +0000</pubDate>
      <link>https://dev.to/yo-shi/lets-stick-to-gmailapp-over-mailapp-in-google-apps-script-d2i</link>
      <guid>https://dev.to/yo-shi/lets-stick-to-gmailapp-over-mailapp-in-google-apps-script-d2i</guid>
      <description>&lt;p&gt;In Google Apps Script (GAS), there are two standard services for sending emails: MailApp and GmailApp. Both features are built-in and can be used immediately in your code without any library setup.&lt;/p&gt;

&lt;p&gt;As mentioned in the title, when I initially used MailApp to implement an external email notification function for a report, emails to domains outside the organization resulted in bounces. After struggling with this and doing some research, I ultimately just switched to GmailApp, and the emails were sent smoothly.&lt;/p&gt;

&lt;p&gt;This experience gave me a deeper understanding of MailApp, GmailApp, and the domain authentication mechanism known as DMARC. I'm leaving this as a memorandum for future reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  MailApp or GmailApp: Which is Better?
&lt;/h2&gt;

&lt;p&gt;The features and suggested use cases for the standard GAS email services are as follows.&lt;br&gt;
As the title suggests, in practical terms, you should prioritize GmailApp if you can use it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;MailApp&lt;/th&gt;
&lt;th&gt;GmailApp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sending Path&lt;/td&gt;
&lt;td&gt;Sends anonymously using a &lt;strong&gt;shared Google server&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Sends directly using the &lt;strong&gt;script-executing user's Gmail account&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benefits&lt;/td&gt;
&lt;td&gt;- Basic email sending is possible&lt;br&gt; - Narrow scope, requires minimal permission granting&lt;/td&gt;
&lt;td&gt;- Reliable external sending is possible&lt;br&gt; - Allows advanced sending like aliases (alternate addresses), HTML emails, and attachments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drawbacks&lt;/td&gt;
&lt;td&gt;- Prone to bouncing on external servers&lt;/td&gt;
&lt;td&gt;- Requires granting a broad range of user permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MailApp is a simple communication method designed for the purpose of "just sending an email" with minimal permissions. Because of this, external servers with strict security checks tend to regard the sender as anonymous and are more likely to reject (bounce) the email.&lt;/p&gt;

&lt;p&gt;GmailApp sends the email from the user's authenticated mailbox (Sent folder), which gives it higher sending reliability and &lt;strong&gt;reduces the risk of bounces&lt;/strong&gt;. However, using it requires broad permissions to access the user's entire Gmail data.&lt;/p&gt;

&lt;p&gt;My initial use of MailApp in this case was simply because "it doesn't show a huge authorization UI for granting Gmail access." I thought simplifying the user flow might reduce the need for user explanations or support inquiries.&lt;/p&gt;

&lt;p&gt;You might consider using MailApp if the email recipients are all within the same domain (for a fully internal report) or if permission management is so strict that granting Gmail data access is absolutely not possible. Otherwise, using GmailApp is less likely to cause trouble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Did the Emails Bounce?
&lt;/h2&gt;

&lt;p&gt;As mentioned, emails sent via MailApp were rejected when sent externally, and this is due to a mechanism called DMARC (Domain-based Message Authentication, Reporting, and Conformance). Furthermore, DMARC acts like a supervisor that leverages the results of two authentication methods, SPF and DKIM, to determine the final processing (action).&lt;/p&gt;

&lt;p&gt;Therefore, we must first understand SPF and DKIM, which are responsible for the actual authentication process.&lt;/p&gt;

&lt;p&gt;Both are authentication technologies designed to prevent "spoofed emails," but they fundamentally differ in what they verify.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Authentication Technology&lt;/th&gt;
&lt;th&gt;Verification Target&lt;/th&gt;
&lt;th&gt;Mechanism Analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SPF (Sender Policy Framework)&lt;/td&gt;
&lt;td&gt;Validity of the Sender's IP Address&lt;/td&gt;
&lt;td&gt;Checks if the postal &lt;strong&gt;"sender address"&lt;/strong&gt; is on the list of allowed sources for that address.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DKIM (DomainKeys Identified Mail)&lt;/td&gt;
&lt;td&gt;Validity of the Email Content&lt;/td&gt;
&lt;td&gt;Checks if the postal &lt;strong&gt;"envelope"&lt;/strong&gt; has a digital signature applied by the domain (sender) and if it hasn't been tampered with during transit.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  SPF (Sender Policy Framework): Verification of the Sender's IP Address
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feikgmx207ezq9j40zarv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feikgmx207ezq9j40zarv.png" alt="spf" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Sender's Setup: The administrator of the sending domain publishes a special text record called an &lt;strong&gt;SPF record&lt;/strong&gt; on the DNS server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SPF Record: This record specifies that "emails for this domain are only permitted to be sent from mail servers with IP addresses listed in this record."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recipient's Verification: The receiving server checks the domain in the Envelope From (Return-Path) field in the email header and queries the DNS for that domain's SPF record.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authentication: If the IP address of the server that actually sent the email is included in the allowed list of the SPF record, the result is Pass.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  DKIM (DomainKeys Identified Mail): Verification by Digital Signature
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi4cboqf0pqzg7u86maww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi4cboqf0pqzg7u86maww.png" alt="dkim" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Sender's Setup: The administrator of the sending domain registers a public key on the DNS server and stores a private key on the mail sending server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signature Application: The sending server uses the private key to encrypt part of the email header and body, and attaches that encrypted data (a digital signature) to the email header.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recipient's Verification: The receiving server checks the signature in the header and queries the DNS for the public key associated with the domain used for the signature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authentication: The digital signature is decrypted with the public key. If it matches the original email content, the result is Pass. If it does not match (e.g., if the content was tampered with), the result is Fail.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  DMARC and the SPF/DKIM Relationship
&lt;/h3&gt;

&lt;p&gt;DMARC is the framework that utilizes the authentication results of SPF and DKIM to determine the final processing (action).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Alignment Check: DMARC's most critical function is to check if the domain used for SPF or DKIM authentication aligns with the domain in the From address that the user sees in their email client.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Policy Setting: The sending domain's administrator publishes a DMARC policy on the DNS, instructing the recipient on "how to handle emails that fail the SPF and DKIM alignment check." The policy primarily includes the following three options:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&amp;nbsp; &amp;nbsp;- p=none: Take no action and send a report to the sender.&lt;br&gt;
&amp;nbsp; &amp;nbsp;- p=quarantine: Place the email in the spam folder (quarantine).&lt;br&gt;
&amp;nbsp; &amp;nbsp;- p=reject: Refuse to accept the email (bounce it).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reporting Feature: DMARC sends detailed reports (DSN reports) to the sending domain's administrator about failed authentications, including where the emails came from. This allows for a grasp of the spoofing situation and improvement of authentication settings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In conclusion, SPF and DKIM are the individual authentication "tools," while DMARC acts as the &lt;strong&gt;"rulebook"&lt;/strong&gt; that synthesizes the results from those tools to ultimately decide and control "whether to accept, discard, or how to process this email."&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools Used for the Investigation
&lt;/h2&gt;

&lt;p&gt;Here are the tools I utilized during this investigation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Email Log Search&lt;/li&gt;
&lt;li&gt;Accessed from the Admin console with Google Workspace Admin privileges.&lt;/li&gt;
&lt;li&gt;Allows checking the delivery status of a selected email.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe3xiyzjea6wicq7eubsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe3xiyzjea6wicq7eubsl.png" alt="log search" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SPF Record Check Tool&lt;/li&gt;
&lt;li&gt;Used &lt;a href="https://mxtoolbox.com/spf.aspx" rel="noopener noreferrer"&gt;mxtoolbox.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;An easy way to check the publicly available DNS information for an SPF record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flnnnpwyrymen3dpaa3a6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flnnnpwyrymen3dpaa3a6.png" alt="mxtool" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DMARC Aggregate Report&lt;/li&gt;
&lt;li&gt;Set up a custom DNS record with the value: &lt;code&gt;"v=DMARC1; p=none; rua=mailto:admin@example.com"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When an email is bounced, a report detailing the rejection reason, similar to the one below, is sent to &lt;code&gt;admin@example.com&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;feedback&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;report_metadata&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;org_name&amp;gt;&lt;/span&gt;google.com&lt;span class="nt"&gt;&amp;lt;/org_name&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;email&amp;gt;&lt;/span&gt;noreply-dmarc-support@google.com&lt;span class="nt"&gt;&amp;lt;/email&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;extra_contact_info&amp;gt;&lt;/span&gt;[https://support.google.com/a/answer/2466580](https://support.google.com/a/answer/2466580)&lt;span class="nt"&gt;&amp;lt;/extra_contact_info&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;report_id&amp;gt;&lt;/span&gt;7457835210456705671&lt;span class="nt"&gt;&amp;lt;/report_id&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;date_range&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;begin&amp;gt;&lt;/span&gt;1762387200&lt;span class="nt"&gt;&amp;lt;/begin&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;end&amp;gt;&lt;/span&gt;1762473599&lt;span class="nt"&gt;&amp;lt;/end&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/date_range&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/report_metadata&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;policy_published&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;domain&amp;gt;&lt;/span&gt;sample.com&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;adkim&amp;gt;&lt;/span&gt;r&lt;span class="nt"&gt;&amp;lt;/adkim&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;aspf&amp;gt;&lt;/span&gt;r&lt;span class="nt"&gt;&amp;lt;/aspf&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;none&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;sp&amp;gt;&lt;/span&gt;none&lt;span class="nt"&gt;&amp;lt;/sp&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;pct&amp;gt;&lt;/span&gt;100&lt;span class="nt"&gt;&amp;lt;/pct&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;np&amp;gt;&lt;/span&gt;none&lt;span class="nt"&gt;&amp;lt;/np&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/policy_published&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;record&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;row&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;source_ip&amp;gt;&lt;/span&gt;209.85.222.44&lt;span class="nt"&gt;&amp;lt;/source_ip&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;count&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/count&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;policy_evaluated&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;disposition&amp;gt;&lt;/span&gt;none&lt;span class="nt"&gt;&amp;lt;/disposition&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dkim&amp;gt;&lt;/span&gt;pass&lt;span class="nt"&gt;&amp;lt;/dkim&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;spf&amp;gt;&lt;/span&gt;pass&lt;span class="nt"&gt;&amp;lt;/spf&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/policy_evaluated&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/row&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;identifiers&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;header_from&amp;gt;&lt;/span&gt;sample.com&lt;span class="nt"&gt;&amp;lt;/header_from&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/identifiers&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;auth_results&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;dkim&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;domain&amp;gt;&lt;/span&gt;sample.com&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;result&amp;gt;&lt;/span&gt;pass&lt;span class="nt"&gt;&amp;lt;/result&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;selector&amp;gt;&lt;/span&gt;google&lt;span class="nt"&gt;&amp;lt;/selector&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/dkim&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;spf&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;domain&amp;gt;&lt;/span&gt;sample.com&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;result&amp;gt;&lt;/span&gt;pass&lt;span class="nt"&gt;&amp;lt;/result&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/spf&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/auth_results&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/record&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/feedback&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Although the initial use of MailApp was unintentional, as written at the beginning, this experience provided valuable knowledge about the best practice of using GmailApp and an understanding of email authentication technology. So, it was a good experience overall.&lt;/p&gt;

&lt;p&gt;I hope to become a developer who can, in the future, intentionally propose MailApp as an option when the situation calls for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://support.google.com/a/answer/81126?sjid=7430220171039891132-NC&amp;amp;visit_id=638983538928114303-2142205423&amp;amp;rd=1" rel="noopener noreferrer"&gt;Google Work Space Admin Help: Email sender guidelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.google.com/mail/answer/6596?visit_id=638983538927207777-342717254&amp;amp;rd=1" rel="noopener noreferrer"&gt;Gmail Help: Fix bounced or rejected emails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gas</category>
      <category>beginners</category>
      <category>security</category>
      <category>learning</category>
    </item>
    <item>
      <title>How I Automated Membership Fee Collection Using Stripe and Google Services</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Sun, 19 Oct 2025 00:25:19 +0000</pubDate>
      <link>https://dev.to/yo-shi/how-i-automated-membership-fee-collection-using-stripe-and-google-services-2gd4</link>
      <guid>https://dev.to/yo-shi/how-i-automated-membership-fee-collection-using-stripe-and-google-services-2gd4</guid>
      <description>&lt;p&gt;I’m in charge of the IT department for a volunteer organization with about 500 members. One day, I was shocked to learn that all the fee collection was being done manually. Even though it’s an annual membership system, the team was relying on manpower at the start of each year, and even mid-year registrations were handled by hand. A quaint, old-fashioned operation manual still existed. So, I decided to build a system that automates all collection, reminders, and aggregation.&lt;/p&gt;

&lt;p&gt;The end users are the treasurers, who change every year and have varying levels of IT skills. Therefore, I made the interface &lt;strong&gt;as simple as possible&lt;/strong&gt;. To issue invoices, they just need to enter the member ID into a spreadsheet. The system automatically generates a list of unpaid members, including their names, amounts, email addresses, and payment URLs.&lt;/p&gt;

&lt;p&gt;In other words, the only touchpoint is the spreadsheet — everything else happens automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

&lt;p&gt;Below is an overview diagram of the payment system architecture.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A trigger is issued from the invoice issuance list.&lt;/li&gt;
&lt;li&gt;Through GAS, invoice data is stored in the BigQuery “invoice” table.&lt;/li&gt;
&lt;li&gt;A timer sends emails containing invoice URLs and updates the unpaid list.&lt;/li&gt;
&lt;li&gt;The user clicks the payment URL.&lt;/li&gt;
&lt;li&gt;Cloud Run verifies the secret and issues the invoice via the Stripe API.&lt;/li&gt;
&lt;li&gt;The user completes the payment on the Stripe invoice page.&lt;/li&gt;
&lt;li&gt;Stripe sends a payment-completed webhook to the Cloud Run endpoint.&lt;/li&gt;
&lt;li&gt;The system updates the payment status in both BigQuery and the spreadsheet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqj70zpj1f23sjeaxciat.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqj70zpj1f23sjeaxciat.png" alt="Payment system architecture diagram" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  System Components
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cloud Run (Checkout Service)
&lt;/h3&gt;

&lt;p&gt;All sensitive information, such as Stripe secret keys, is managed through environment variables and securely accessed via Secret Manager.&lt;br&gt;
Payment links are protected using HMAC signatures and time-limited URLs to prevent unauthorized access.&lt;br&gt;
Additionally, hashed audit data (such as IP addresses and idempotency keys) are stored in BigQuery, enabling both re-execution prevention and traceability.&lt;/p&gt;




&lt;h3&gt;
  
  
  GAS Pipeline
&lt;/h3&gt;

&lt;p&gt;The GAS operates from within a spreadsheet, reading environment settings (test/live) and terms directly from the sheet.&lt;br&gt;
It embeds signed links into template emails and sends them, while all sent records are logged in the &lt;code&gt;comms&lt;/code&gt; dataset.&lt;br&gt;
When a payment deadline passes, webhook alerts are triggered and automatically recorded in the designated spreadsheet — effectively acting as a “watchdog for unpaid invoices.”&lt;/p&gt;




&lt;h3&gt;
  
  
  BigQuery
&lt;/h3&gt;

&lt;p&gt;BigQuery functions as both an &lt;strong&gt;invoice ledger&lt;/strong&gt; and a &lt;strong&gt;payment tracking system&lt;/strong&gt;, generating invoice data from existing user records.&lt;br&gt;
When a link is clicked, payment history is checked using the corresponding &lt;code&gt;invoice_id&lt;/code&gt;.&lt;br&gt;
With the &lt;code&gt;MERGE&lt;/code&gt; statement, it performs &lt;strong&gt;“update if exists, insert if not”&lt;/strong&gt;, ensuring that only one invoice record exists per user.&lt;br&gt;
Separate datasets are used for each environment, allowing seamless switching without code modification.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Because of its sensitive nature, the payment system required special attention in several areas. The main points are summarized below.&lt;/p&gt;

&lt;h3&gt;
  
  
  HMAC Signatures and Time-Limited Links
&lt;/h3&gt;

&lt;p&gt;Since payment links include user identification data, tampering could redirect a payment to another person.&lt;br&gt;&lt;br&gt;
To prevent this, Cloud Run adds an &lt;strong&gt;HMAC signature&lt;/strong&gt; when generating each link.&lt;br&gt;
&lt;strong&gt;HMAC (Hash-based Message Authentication Code)&lt;/strong&gt; uses a shared secret to sign message content.&lt;br&gt;
The &lt;code&gt;invoice_id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, and expiration timestamp are bundled and signed, then verified by Cloud Run.&lt;br&gt;&lt;br&gt;
If any part of the URL is modified, it becomes invalid immediately.&lt;br&gt;
Naturally, the signing key is securely stored in Secret Manager.&lt;/p&gt;




&lt;h3&gt;
  
  
  Difference Between Stripe Session and Invoice
&lt;/h3&gt;

&lt;p&gt;Initially, invoices were created on the Stripe side, and payment URLs were emailed to users.&lt;br&gt;
However, these links expired after 24 hours — an unexpected limitation.&lt;br&gt;
Further research revealed that Stripe provides two types of payment objects: Session and Invoice.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**Using Invoice&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment links never expire.&lt;/li&gt;
&lt;li&gt;Payment status updates automatically and is visible on the dashboard.&lt;/li&gt;
&lt;li&gt;Stripe can automatically send reminders to customers.&lt;/li&gt;
&lt;li&gt;However, external integrations and custom signing are limited.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;**Using Session&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A lightweight, temporary payment page object (typically valid for 24 hours).&lt;/li&gt;
&lt;li&gt;Allows full developer control, including HMAC-signed links and BigQuery integration.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;While Invoice offers strong management features, it leads to duplicate bookkeeping between Stripe’s customer DB and the internal ledger — and it also incurs extra fees.&lt;/p&gt;

&lt;p&gt;Therefore, this project chose the Session-based approach for its flexibility and integration capability.&lt;br&gt;
Stripe serves as the payment gateway, while BigQuery and Cloud Run handle billing management — a clear division of responsibilities.  &lt;/p&gt;




&lt;h3&gt;
  
  
  Unique Payment Links Generated by Cloud Run
&lt;/h3&gt;

&lt;p&gt;Each time a user opens the link, Cloud Run requests Stripe to create a new Session.&lt;br&gt;
To prevent duplicate charges, it includes an idempotency key formatted as &lt;code&gt;stripe_idempotency_prefix:invoice_id&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
Stripe returns the same result for repeated requests with the same key, ensuring that the user is never charged more than once, even if the button is clicked multiple times.&lt;/p&gt;




&lt;h3&gt;
  
  
  BigQuery as the “Billing Ledger”
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;invoice_id&lt;/code&gt; serves as the unique key in BigQuery for managing payment states.&lt;br&gt;
Before completion, the status is “Unpaid.” Once the Stripe webhook sends a payment notification, the status updates to “Paid.”&lt;br&gt;
Even if the process runs multiple times, only the existing record is updated — no duplicates occur.&lt;br&gt;&lt;br&gt;
Thanks to the use of the &lt;code&gt;MERGE&lt;/code&gt; statement, the ledger always reflects the most accurate and up-to-date state.  &lt;/p&gt;




&lt;h3&gt;
  
  
  Link Reissue and Expiration
&lt;/h3&gt;

&lt;p&gt;Since Stripe Sessions cannot be extended, Cloud Run regenerates a new Session using the same billing information.&lt;br&gt;
Technically, it’s a “recreation,” but in practice, it works as a “reissued payment link” for the same invoice.&lt;br&gt;&lt;br&gt;
This approach ensures that even expired links always lead users to a valid, active payment flow.&lt;/p&gt;




&lt;h3&gt;
  
  
  Cloud Run Webhook Reception and Automation
&lt;/h3&gt;

&lt;p&gt;Cloud Run also serves as the webhook endpoint for Stripe.&lt;/p&gt;

&lt;p&gt;When Stripe sends a payment completion event, Cloud Run receives it and updates BigQuery using the &lt;code&gt;invoice_id&lt;/code&gt; and &lt;code&gt;family_id&lt;/code&gt; included in the &lt;code&gt;metadata&lt;/code&gt;.&lt;br&gt;
As a result, the Google Workspace side can monitor payment status in real time.&lt;br&gt;
Unpaid lists, reminder emails, and even chat notifications are fully automated — the tedious task of manually checking “who hasn’t paid yet” is now a thing of the past.&lt;/p&gt;




&lt;h3&gt;
  
  
  Test Environment
&lt;/h3&gt;

&lt;p&gt;Stripe provides a sandbox environment for testing, allowing separate &lt;code&gt;api_key&lt;/code&gt; and &lt;code&gt;webhook_secret&lt;/code&gt; for production and testing.&lt;/p&gt;

&lt;p&gt;The GAS side is designed to switch environments easily.&lt;br&gt;&lt;br&gt;
By toggling a single script property (&lt;code&gt;live/test&lt;/code&gt;), the environment can be changed instantly.&lt;br&gt;
BigQuery is divided into two datasets — &lt;code&gt;finance&lt;/code&gt; and &lt;code&gt;finance_test&lt;/code&gt; — with identical schemas for safe testing.&lt;br&gt;
Cloud Run uses the same container image, switching only the keys via Secret Manager.&lt;br&gt;
This design enables environment switching without any code modification.&lt;/p&gt;




&lt;h3&gt;
  
  
  Performance Verification
&lt;/h3&gt;

&lt;p&gt;Since this system operates in a fully serverless Cloud Run environment, cold-start latency may occur on the first access.&lt;br&gt;
Therefore, an actual load test was conducted using a Python (&lt;code&gt;aiohttp&lt;/code&gt;) script under the following conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spike test: 50 concurrent requests
&lt;/li&gt;
&lt;li&gt;Distributed test: 30 requests over 20 seconds (with jitter)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All requests — including those under cold-start conditions — were successfully processed, with no timeouts or 5xx errors.&lt;br&gt;
Although Cloud Run’s free tier charges for requests triggered from a cold start, the expected scale of about 500 members poses no operational issues in practice.  &lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The system operates in a &lt;strong&gt;three-layer structure&lt;/strong&gt;:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;BigQuery&lt;/strong&gt; serves as the &lt;em&gt;ledger&lt;/em&gt;, &lt;strong&gt;Cloud Run&lt;/strong&gt; handles &lt;em&gt;control and integration&lt;/em&gt;, and &lt;strong&gt;Stripe&lt;/strong&gt; acts as the &lt;em&gt;payment processor&lt;/em&gt;.&lt;br&gt;
The &lt;code&gt;invoice_id&lt;/code&gt; ensures uniqueness on the ledger side, while the &lt;code&gt;idempotency_key&lt;/code&gt; guarantees uniqueness on the Stripe side.&lt;br&gt;
Additionally, &lt;strong&gt;HMAC-signed links&lt;/strong&gt; secure every transaction.&lt;/p&gt;

&lt;p&gt;From unpaid checks to session creation and webhook synchronization, the entire flow runs automatically, ensuring &lt;strong&gt;accurate and safe one-time payments&lt;/strong&gt;, even if the same link is opened multiple times.&lt;/p&gt;

&lt;p&gt;Many parts of this implementation were built with the help of generative AI, and the experience of working with payment architecture — including the differences between Stripe Session and Invoice and API idempotency — has been highly valuable.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://osamadev.medium.com/idempotency-in-apis-making-sure-api-requests-are-safe-and-reliable-7d5cb51520fe" rel="noopener noreferrer"&gt;Idempotency in APIs: Making Sure API Requests Are Safe and Reliable&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.stripe.com/questions/stripe-invoicing-pricing" rel="noopener noreferrer"&gt;Stripe Invoicing pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>stripe</category>
      <category>googlecloud</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Exploring the World of Packets with Scapy: A Beginner’s Hands-on Journey to Understanding</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Wed, 24 Sep 2025 03:10:47 +0000</pubDate>
      <link>https://dev.to/yo-shi/exploring-the-world-of-packets-with-scapy-a-beginners-hands-on-journey-to-understanding-1d0g</link>
      <guid>https://dev.to/yo-shi/exploring-the-world-of-packets-with-scapy-a-beginners-hands-on-journey-to-understanding-1d0g</guid>
      <description>&lt;h2&gt;
  
  
  Playing with Packets
&lt;/h2&gt;

&lt;p&gt;I had a conceptual understanding of how each layer of the network works, but the actual picture was still vague, and my understanding wasn’t progressing much. That’s when I came across a tool called &lt;strong&gt;Scapy&lt;/strong&gt;. With Scapy, you can &lt;em&gt;create packets&lt;/em&gt;, &lt;em&gt;observe them&lt;/em&gt;, and &lt;em&gt;trace their routes&lt;/em&gt;... or so I heard. But I wasn’t sure what I could really learn by observing packets, so I decided to try it myself.&lt;/p&gt;

&lt;p&gt;I installed Scapy in a Python virtual environment and experimented with &lt;strong&gt;ping → capture → traceroute → ARP scan&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Here, I’ll share the questions I had and the discoveries that made me say, “Oh, I see!”&lt;/p&gt;
&lt;h2&gt;
  
  
  What is Scapy?
&lt;/h2&gt;

&lt;p&gt;Scapy is a &lt;strong&gt;packet manipulation library&lt;/strong&gt; available in Python.&lt;/p&gt;

&lt;p&gt;With it, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create&lt;/strong&gt; packets
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send&lt;/strong&gt; packets
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture (read)&lt;/strong&gt; packets
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyze&lt;/strong&gt; packets
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s a very handy tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commonly used classes&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IP()&lt;/code&gt; : IP header
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TCP()&lt;/code&gt; : TCP header
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UDP()&lt;/code&gt; : UDP header
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rdpcap()&lt;/code&gt; : read pcap files
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wrpcap()&lt;/code&gt; : write pcap files
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;If you want to view packets with a GUI, there’s a tool called &lt;strong&gt;Wireshark&lt;/strong&gt;. Both Scapy and Wireshark can read and write packet capture files (pcap).&lt;/p&gt;

&lt;p&gt;Wireshark:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GUI-based operations
&lt;/li&gt;
&lt;li&gt;Decodes and displays packets in a human-readable format
&lt;/li&gt;
&lt;li&gt;Great for analysis with clicks and filters
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scapy:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python library
&lt;/li&gt;
&lt;li&gt;Can create, analyze, and send packets programmatically
&lt;/li&gt;
&lt;li&gt;Strong for automation and experiments
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I once tried looking at packets with Wireshark, but back then I didn’t understand much and gave up—a bitter memory.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. What is a Packet?
&lt;/h2&gt;

&lt;p&gt;A packet, as the name suggests, is like a small parcel on the network—a &lt;strong&gt;small fragment of communication&lt;/strong&gt;. Large data is divided into small packets and sent across the network.&lt;/p&gt;

&lt;p&gt;Here’s a quick recap of the OSI reference model (L2, L3, L4):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Ethernet (L2)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Address label of a delivery service inside a LAN (MAC address)
&lt;/li&gt;
&lt;li&gt;Decides &lt;em&gt;which device&lt;/em&gt; in the same network to deliver to
&lt;/li&gt;
&lt;li&gt;The MAC gets rewritten when passing through a router
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IP (L3)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Address of the Internet world (IP address)
&lt;/li&gt;
&lt;li&gt;Like deciding the destination country or city
&lt;/li&gt;
&lt;li&gt;Reaches faraway places through routing
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TCP (L4)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rules for reliable communication
&lt;/li&gt;
&lt;li&gt;Establishes a connection (3-way handshake)
&lt;/li&gt;
&lt;li&gt;Numbers data (sequence numbers)
&lt;/li&gt;
&lt;li&gt;Confirms delivery (ACK)
&lt;/li&gt;
&lt;li&gt;Retransmits if something is missing
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ethernet is LAN-local and can only reach up to the next router.&lt;br&gt;&lt;br&gt;
But inside it, the “parcel label” (IP address) always has the same src/dst info, so the packet can keep being forwarded router by router until it reaches the final destination.&lt;/p&gt;

&lt;p&gt;Scapy’s coverage looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;L2 (Data Link Layer)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create Ethernet frames
&lt;/li&gt;
&lt;li&gt;Specify MAC addresses
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;L3 (Network Layer)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate IP packets
&lt;/li&gt;
&lt;li&gt;Edit IP headers
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;L4 (Transport Layer)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Work with TCP/UDP/ICMP headers
&lt;/li&gt;
&lt;li&gt;Specify port numbers
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;L5 (Session Layer equivalent)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can sometimes create protocols closer to the application layer
&lt;/li&gt;
&lt;li&gt;But not clearly defined as full L5 support
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ethernet (L2), IP (L3), and TCP/UDP/ICMP (L4) are &lt;strong&gt;standardized by IETF/IEEE&lt;/strong&gt; with clearly defined header formats and fields. Scapy can build and parse packets according to those standards.&lt;/p&gt;

&lt;p&gt;On the other hand, the session, presentation, and application layers are often &lt;strong&gt;application-specific&lt;/strong&gt; and diverse, even if partially standardized. Scapy supports some of these, but it’s not its main strength. (Honestly, I haven’t fully explored how far it can go yet.)&lt;/p&gt;

&lt;p&gt;As explained above, a single packet often shows only a &lt;strong&gt;small fragment&lt;/strong&gt; of communication. Still, even looking at just one packet revealed some surprisingly interesting findings.&lt;/p&gt;
&lt;h3&gt;
  
  
  summary()
&lt;/h3&gt;

&lt;p&gt;Let’s try capturing packets with Scapy by running &lt;code&gt;test_summary.py&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scapy.all&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sniff&lt;/span&gt;

&lt;span class="c1"&gt;# Capture only 10 packets
&lt;/span&gt;&lt;span class="n"&gt;packets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sniff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Display a simple summary
&lt;/span&gt;&lt;span class="n"&gt;packets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sniff()&lt;/code&gt; reads raw packets directly, so on Linux it requires &lt;strong&gt;root privileges&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
For this test, I gave permission using &lt;code&gt;sudo&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /home/user/sandbox/network/venv/bin/python /home/user/sandbox/network/test_summary.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ether / IP / TCP 192.0.2.25:46924 &amp;gt; 203.0.113.4:https S
Ether / IP / TCP 203.0.113.4:https &amp;gt; 192.0.2.25:46924 SA
Ether / IP / TCP 192.0.2.25:46924 &amp;gt; 203.0.113.4:https A
Ether / IP / TCP 192.0.2.25:46924 &amp;gt; 203.0.113.4:https PA / Raw
Ether / IP / TCP 203.0.113.4:https &amp;gt; 192.0.2.25:46924 A / Raw
Ether / IP / TCP 192.0.2.25:46924 &amp;gt; 203.0.113.4:https A
Ether / IP / TCP 192.0.2.25:46924 &amp;gt; 203.0.113.4:https PA / Raw
Ether / IP / TCP 203.0.113.4:https &amp;gt; 192.0.2.25:46924 PA / Raw
Ether / IP / TCP 203.0.113.4:https &amp;gt; 192.0.2.25:46924 PA / Raw
Ether / IP / TCP 192.0.2.25:46924 &amp;gt; 203.0.113.4:https A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We were able to confirm a summary of the first 10 packets.&lt;/p&gt;

&lt;p&gt;This list shows the flow from the start of the connection (handshake) to the actual data exchange in a concise way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S&lt;/strong&gt; = SYN
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SA&lt;/strong&gt; = SYN+ACK
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A&lt;/strong&gt; = ACK
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P&lt;/strong&gt; = PSH
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw&lt;/strong&gt; = payload present
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sequence is: S → SA → A (3-way handshake) → then data (PA / Raw, etc.)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;192.0.2.25:46924 &amp;gt; 203.0.113.4:https S&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Client → Server: SYN  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;203.0.113.4:https &amp;gt; 192.0.2.25:46924 SA&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Server → Client: SYN+ACK  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;192.0.2.25:46924 &amp;gt; 203.0.113.4:https A&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Client → Server: ACK&lt;br&gt;&lt;br&gt;
→ This completes the &lt;strong&gt;3-way handshake&lt;/strong&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that, &lt;code&gt;PA / Raw&lt;/code&gt; indicates data transfer (Push + ACK + payload).&lt;br&gt;&lt;br&gt;
A packet with only &lt;code&gt;A&lt;/code&gt; is just an ACK response.&lt;br&gt;&lt;br&gt;
The 3-way handshake described in textbooks was clearly confirmed here through the capture.&lt;/p&gt;
&lt;h3&gt;
  
  
  show()
&lt;/h3&gt;

&lt;p&gt;Let’s take a closer look at the first packet we captured earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;packets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;###[ Ethernet ]###
  dst       = 00:11:22:33:44:55
  src       = 66:77:88:99:aa:bb
  type      = IPv4
###[ IP ]###
     version   = 4
     ihl       = 5
     tos       = 0x0
     len       = 60
     id        = 65307
     flags     = DF
     frag      = 0
     ttl       = 64
     proto     = tcp
     chksum    = 0x6ffc
     src       = 192.0.2.25
     dst       = 203.0.113.4
     \options   \
###[ TCP ]###
        sport     = 46924
        dport     = https
        seq       = 2126599486
        ack       = 0
        dataofs   = 10
        reserved  = 0
        flags     = S
        window    = 64860
        chksum    = 0xcbd2
        urgptr    = 0
        options   = [('MSS', 1380), ('SAckOK', b''), ('Timestamp', (3644677925, 0)), ('NOP', None), ('WScale', 7)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the first SYN packet of the TCP 3-way handshake.&lt;br&gt;&lt;br&gt;
Next, the server replies with SYN+ACK, and then the client sends back ACK to complete the connection.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ether&lt;/strong&gt;: src/dst are the sender/receiver MAC addresses
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP&lt;/strong&gt;: src/dst are the sender/receiver IP addresses, &lt;code&gt;ttl&lt;/code&gt; is time-to-live, &lt;code&gt;DF&lt;/code&gt; means no fragmentation
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;sport/dport&lt;/strong&gt;: source/destination ports (https = 443)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;seq&lt;/strong&gt;: initial sequence number of this packet
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ack&lt;/strong&gt;: 0 because this is SYN
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;flags&lt;/strong&gt;: S (SYN)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;window&lt;/strong&gt;: receive window size
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;options&lt;/strong&gt;: MSS (maximum segment size), SAckOK (SACK allowed), Timestamp, WScale (window scaling)
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;※ &lt;code&gt;192.0.2.25:46924 → 203.0.113.4:443&lt;/code&gt; represents client → server communication.&lt;/p&gt;

&lt;p&gt;Meaning of TCP fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;flags = S&lt;/code&gt; → SYN flag is set. The first “Can I connect?” signal.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;seq = 2126599486&lt;/code&gt; → Initial sequence number chosen by the client. The server uses this as the basis for communication.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ack = 0&lt;/code&gt; → No acknowledgment expected yet.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sport = 46924 / dport = https(443)&lt;/code&gt; → Client chose random source port 46924 → server’s HTTPS port 443.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: &lt;strong&gt;Ethernet&lt;/strong&gt; delivers within the LAN, &lt;strong&gt;IP&lt;/strong&gt; delivers across the Internet, and &lt;strong&gt;TCP&lt;/strong&gt; ensures reliability.&lt;br&gt;&lt;br&gt;
All of these are stacked together inside a single packet.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Sending a Ping with Scapy
&lt;/h2&gt;

&lt;p&gt;Sometimes you may wonder, &lt;em&gt;“Is the network connection really working?”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
In such cases, the familiar &lt;strong&gt;Ping&lt;/strong&gt; comes in handy.&lt;br&gt;&lt;br&gt;
Let’s try doing a Ping using Scapy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scapy.all&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ICMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr1&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Create a packet
&lt;/span&gt;&lt;span class="n"&gt;pkt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8.8.8.8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;ICMP&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Check the contents of the packet
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=== Sent Packet ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Send it and wait for a reply
&lt;/span&gt;&lt;span class="n"&gt;ans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sr1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 4. Check the reply packet
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;=== Reply Packet ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== Sent Packet ===
###[ IP ]###
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     = 
  frag      = 0
  ttl       = 64
  proto     = icmp
  chksum    = None
  src       = 192.0.2.25
  dst       = 8.8.8.8
  \options   \
###[ ICMP ]###
     type      = echo-request
     code      = 0
     chksum    = None
     id        = 0x0
     seq       = 0x0
     unused    = b''

Begin emission
.
Finished sending 1 packets
*
Received 2 packets, got 1 answers, remaining 0 packets

=== Reply Packet ===
###[ IP ]###
  version   = 4
  ihl       = 5
  tos       = 0x0
  len       = 28
  id        = 0
  flags     = 
  frag      = 0
  ttl       = 114
  proto     = icmp
  chksum    = 0x6984
  src       = 8.8.8.8
  dst       = 192.0.2.25
  \options   \
###[ ICMP ]###
     type      = echo-reply
     code      = 0
     chksum    = 0xffff
     id        = 0x0
     seq       = 0x0
     unused    = b''
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sent Packet (echo-request)&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP Header&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;src = 192.0.2.25 (my local PC)
&lt;/li&gt;
&lt;li&gt;dst = 8.8.8.8 (Google DNS)
&lt;/li&gt;
&lt;li&gt;proto = icmp
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;ICMP Header&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;type = echo-request (Ping “please respond”)
&lt;/li&gt;
&lt;li&gt;id / seq = 0x0 (identification number)
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reply Packet (echo-reply)&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP Header&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;src = 8.8.8.8 (response from the other side)
&lt;/li&gt;
&lt;li&gt;dst = 192.0.2.25 (sent back to my PC)
&lt;/li&gt;
&lt;li&gt;ttl = 114 (went through several routers)
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;ICMP Header&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;type = echo-reply (Ping “reply”)
&lt;/li&gt;
&lt;li&gt;id / seq = 0x0 (same as the request → proof of proper matching)
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In the code, the &lt;code&gt;/&lt;/code&gt; operator means &lt;strong&gt;“stack layers”&lt;/strong&gt;. Think of wrapping Ethernet/IP/TCP layers together.&lt;br&gt;&lt;br&gt;
Sending an &lt;code&gt;echo-request&lt;/code&gt; and receiving an &lt;code&gt;echo-reply&lt;/code&gt; felt impressive.&lt;br&gt;&lt;br&gt;
Also, the reply packet showed &lt;code&gt;ttl = 114&lt;/code&gt;. I had read about TTL in theory, but seeing that number in practice made me wonder—why exactly 114?&lt;/p&gt;
&lt;h3&gt;
  
  
  What is TTL?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Time To Live (TTL)&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A counter that &lt;strong&gt;decreases by 1 every time an IP packet passes through a router&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;When it reaches &lt;strong&gt;0, the packet is discarded&lt;/strong&gt; (to prevent infinite loops)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, &lt;code&gt;ttl = 114&lt;/code&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google’s server (8.8.8.8) likely sent the packet with an &lt;strong&gt;initial value of 128&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;When it reached my PC, it was &lt;strong&gt;114&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;→ 128 - 114 = &lt;strong&gt;passed through 14 routers&lt;/strong&gt; (hops)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important Notes&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TTL itself is &lt;strong&gt;not the number of routers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Formula: &lt;strong&gt;Initial value − Received TTL = Number of routers passed&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Typical defaults: Windows = 128, Linux/Unix = 64, Cisco = 255
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, TTL helps us estimate the number of hops a packet has traveled.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Tracing the Route with Traceroute
&lt;/h2&gt;

&lt;p&gt;Next, let’s try &lt;strong&gt;traceroute&lt;/strong&gt; (which Scapy can also do) to see the actual path of the packets.&lt;br&gt;&lt;br&gt;
Scapy provides a built-in &lt;code&gt;traceroute()&lt;/code&gt; function, so I tested it with Google DNS and Yahoo Japan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scapy.all&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traceroute&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;

&lt;span class="c1"&gt;# Destination
&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8.8.8.8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Try only once (retry=0)
&lt;/span&gt;&lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;traceroute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;maxttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=== Traceroute to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;snd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rcv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;
    &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rcv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gethostbyaddr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;herror&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== Traceroute to 8.8.8.8 ===
 1  172.19.32.1      LAPTOP-XXXX.EXAMPLE.net
 2  10.0.0.1         ?
 4  96.xxx.xxx.xxx  router1.isp.example.net
 5  68.xxx.xxx.xxx  router2.isp.example.net
 6  96.xxx.xxx.xxx  router3.isp.example.net
 8  96.xxx.xxx.xxx  router4.isp.example.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;1: 172.19.32.1 → Local LAN gateway (the first router)
&lt;/li&gt;
&lt;li&gt;2: 10.0.0.1 → Provider-side private IP router (NAT or CMTS, etc.)
&lt;/li&gt;
&lt;li&gt;4, 5, 6, 8 → Intermediate ISP routers. Addresses like 96.x.x.x and 68.x.x.x show up (likely Comcast).
&lt;/li&gt;
&lt;li&gt;(3, 7) → Routers that didn’t respond, which is common in traceroute.
&lt;/li&gt;
&lt;li&gt;8.8.8.8 → Final destination, Google Public DNS
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the last hop shows &lt;strong&gt;8.8.8.8&lt;/strong&gt;, we can confirm that the route successfully reached Google DNS.&lt;/p&gt;

&lt;p&gt;This route represents:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;My PC → Local gateway → ISP routers → Internet backbone → Google DNS&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Traceroute works by gradually increasing TTL one by one and collecting the ICMP &lt;em&gt;Time Exceeded&lt;/em&gt; messages from each router along the path.&lt;/p&gt;
&lt;h3&gt;
  
  
  Traceroute to Yahoo Japan
&lt;/h3&gt;

&lt;p&gt;Earlier, the path stayed entirely within the U.S.&lt;br&gt;&lt;br&gt;
But if the destination is set to &lt;code&gt;www.yahoo.co.jp&lt;/code&gt;, you can see the route stretching from the U.S. all the way to Japan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scapy.all&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traceroute&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;

&lt;span class="c1"&gt;# Change destination to Yahoo Japan
&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;www.yahoo.co.jp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Try only once (retry=0)
&lt;/span&gt;&lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;traceroute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;maxttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=== Traceroute to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Sort by TTL for readability
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;snd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rcv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;hop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;
    &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rcv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gethostbyaddr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;herror&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hop&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1  172.19.32.1      LAPTOP-EXAMPLE.mshome.net
 2  10.0.0.1         ?
 4  198.51.100.1     router1.isp.example.net
 5  198.51.100.2     router2.isp.example.net
 6  198.51.100.3     router3.isp.example.net
 7  198.51.100.4     router4.isp.example.net
 8  198.51.100.5     router5.isp.example.net
 9  198.51.100.6     router6.isp.example.net
10  198.51.100.7     router7.isp.example.net
11  198.51.100.8     router8.isp.example.net
12  198.51.100.9     router9.isp.example.net
13  198.51.100.10    router10.isp.example.net
14  198.51.100.11    router11.isp.example.net
15  198.51.100.12    router12.isp.example.net
16  198.51.100.13    router13.isp.example.net
17  198.51.100.14    router14.isp.example.net
18  198.51.100.15    router15.isp.example.net
19  198.51.100.16    router16.isp.example.net
20  198.51.100.17    router17.isp.example.net
21  198.51.100.18    router18.isp.example.net
22  198.51.100.19    router19.isp.example.net
23  198.51.100.20    router20.isp.example.net
26 106.187.13.25     ?
27 27.86.xx.xxx      ?
28 27.86.xx.xxx      ?
36 182.22.xx.xxx     ? 
37 182.22.xx.xxx     ?
38 182.22.xx.xxx     ?

(Same IPs continue below…)
(xxx indicates masked digits)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Path Analysis&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Up to 23&lt;/strong&gt;: Inside the ISP. (In the actual capture it went Chicago → Denver → San Jose → West Coast.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;26: 106.187.13.25, 27–28: 27.86.*&lt;/strong&gt;: Very likely where it enters Japan (carrier/IX).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From 36 onward: 182.22.xx.xxx&lt;/strong&gt;: The same device is replying (effects of multiple probes or ICMP limits). Reachability itself was achieved earlier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;whois 182.22.xx.xxx&lt;/code&gt; shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inetnum:      182.22.0.0 - 182.22.127.255
netname:      YAHOO
descr:        LY Corporation
descr:        1-3 Kioicho, Chiyoda-ku, Tokyo, Japan
country:      JP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;182.22.xx.xxx&lt;/code&gt; addresses that repeatedly appeared at the end of the traceroute belong to the range &lt;code&gt;182.22.0.0 - 182.22.127.255&lt;/code&gt;, which is registered to &lt;strong&gt;LY Corporation&lt;/strong&gt; (formerly Yahoo Japan).&lt;/p&gt;

&lt;p&gt;In other words, we can confirm the path reached:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;USA → Comcast backbone → submarine cable → Japan (LY network)&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Code to check ports on your own PC
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Scanning ports on external sites can violate terms of service or be considered an attack. It’s safer to run these tests against &lt;strong&gt;your own server&lt;/strong&gt; or a &lt;strong&gt;local environment&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scapy.all&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;

&lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# My own PC (localhost)
&lt;/span&gt;&lt;span class="n"&gt;ports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3306&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Ports to test
&lt;/span&gt;
&lt;span class="c1"&gt;# Send SYN packets and wait for replies
&lt;/span&gt;&lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Analyze responses
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;haslayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Port &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;dport&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is OPEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Port &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;dport&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is CLOSED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Decision Rules&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reply with &lt;strong&gt;SYN+ACK&lt;/strong&gt; → Port is &lt;strong&gt;OPEN&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Reply with &lt;strong&gt;RST&lt;/strong&gt; → Port is &lt;strong&gt;CLOSED&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No response → &lt;strong&gt;Blocked by firewall&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my PC, all tested ports were closed.&lt;br&gt;&lt;br&gt;
But after starting a server with &lt;code&gt;python -m http.server 8080&lt;/code&gt; and rescanning, port &lt;strong&gt;8080 showed as OPEN&lt;/strong&gt;, which made perfect sense.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. ARP Scan for LAN Discovery
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ARP (Address Resolution Protocol)&lt;/strong&gt; is the mechanism for asking within a LAN:&lt;br&gt;&lt;br&gt;
&lt;em&gt;“Which MAC address has this IP address?”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
It works at &lt;strong&gt;Layer 2 (Data Link Layer)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To figure out the range of the home LAN (subnet), first check your PC’s current IP address.&lt;br&gt;&lt;br&gt;
In my case (using WSL), running &lt;code&gt;ip addr&lt;/code&gt; showed:&lt;br&gt;&lt;br&gt;
&lt;code&gt;eth0&lt;/code&gt; → &lt;code&gt;172.19.35.58/20&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
So the network range is &lt;code&gt;172.19.32.0/20&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scapy.all&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ARP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Ether&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srp&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;172.19.32.0/20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;zenn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dashboard&lt;/span&gt;
&lt;span class="n"&gt;pkt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Ether&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ff:ff:ff:ff:ff:ff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;ARP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;srp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IP: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;psrc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, MAC: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hwsrc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;===&lt;/span&gt; Devices &lt;span class="k"&gt;in &lt;/span&gt;LAN &lt;span class="o"&gt;===&lt;/span&gt; IP: 172.19.32.1, MAC: 00:15:5d:4a:02:13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only one device showed up. &lt;em&gt;“Huh? There should be more devices on my LAN…”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
The reason is &lt;strong&gt;WSL&lt;/strong&gt;: from WSL you can only see the &lt;strong&gt;virtual gateway&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
WSL’s &lt;code&gt;eth0&lt;/code&gt; (172.19.35.58/20) is actually connected to a &lt;strong&gt;Hyper-V virtual switch&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
So, from inside WSL it looks like there’s only one peer on the same network—the gateway (172.19.32.1).&lt;br&gt;&lt;br&gt;
WSL’s &lt;code&gt;eth0&lt;/code&gt; is a virtual NIC created by Windows. Devices on the real network beyond it &lt;strong&gt;cannot be ARP-scanned&lt;/strong&gt; from WSL.&lt;br&gt;&lt;br&gt;
On a real Linux/Windows host, &lt;code&gt;arp -a&lt;/code&gt; should show the other devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By extracting packets with Scapy, the network became more concrete.&lt;br&gt;&lt;br&gt;
Seeing raw data helped the model click for me: &lt;strong&gt;Ethernet = local addressing&lt;/strong&gt;, &lt;strong&gt;IP = global addressing&lt;/strong&gt;, &lt;strong&gt;TCP = reliability rules&lt;/strong&gt;—each layer with its role.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://scapy.net/" rel="noopener noreferrer"&gt;Scapy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/secdev/scapy" rel="noopener noreferrer"&gt;Scapy: GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qiita.com/Howtoplay/items/4080752d0d8c7a9ef2aa" rel="noopener noreferrer"&gt;Scapy Intro (Qiita) **JAPANESE&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>network</category>
      <category>beginners</category>
    </item>
    <item>
      <title>My Experience Fixing clasp Login Errors on Google Workspace</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Sun, 07 Sep 2025 20:09:52 +0000</pubDate>
      <link>https://dev.to/yo-shi/my-experience-fixing-clasp-login-errors-on-google-workspace-3nh</link>
      <guid>https://dev.to/yo-shi/my-experience-fixing-clasp-login-errors-on-google-workspace-3nh</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When I tried to develop using the Google Apps Script CLI tool &lt;strong&gt;clasp&lt;/strong&gt;, I got stuck on authentication.&lt;br&gt;&lt;br&gt;
The conclusion is that security requirements differ between personal accounts and Google Workspace accounts.  &lt;/p&gt;

&lt;p&gt;Since I had a successful experience with a personal account, I couldn’t understand why errors appeared.&lt;br&gt;&lt;br&gt;
Even when I asked ChatGPT, most of the answers assumed a personal account, which made troubleshooting difficult.  &lt;/p&gt;

&lt;p&gt;So here, I’ll organize the steps required for Workspace accounts as both a reminder for myself and a reference for others.  &lt;/p&gt;
&lt;h2&gt;
  
  
  The Mysterious Error on Workspace Accounts
&lt;/h2&gt;

&lt;p&gt;For personal accounts, the first &lt;code&gt;clasp login&lt;/code&gt; opens a browser and shows the Google authentication screen.&lt;br&gt;&lt;br&gt;
Once approved, the setup is complete, and from the second time on, authentication works instantly using cache.  &lt;/p&gt;
&lt;h3&gt;
  
  
  Personal account (@gmail.com)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clasp login
&lt;span class="c"&gt;# → Authentication completes immediately!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But with a Google Workspace account, it doesn’t work like that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clasp login
&lt;span class="c"&gt;# → "We are sorry, but you do not have access to this service"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Different Authentication Mechanisms for Personal vs Workspace
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Personal Account (simple)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Uses Google’s default credentials
&lt;/li&gt;
&lt;li&gt;Few restrictions, no admin approval required
&lt;/li&gt;
&lt;li&gt;Just &lt;code&gt;clasp login&lt;/code&gt; works
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Workspace Account (complex)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;External apps are blocked by security policy
&lt;/li&gt;
&lt;li&gt;Explicit administrator approval is required
&lt;/li&gt;
&lt;li&gt;Custom OAuth credentials must be created
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5 Steps
&lt;/h2&gt;

&lt;p&gt;To resolve the login issue with a Google Workspace account, the following five steps were necessary:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Allow API access in the Admin Console (organization-level approval)
&lt;/li&gt;
&lt;li&gt;Enable the Apps Script API in Google Cloud Console (project-level activation)
&lt;/li&gt;
&lt;li&gt;Configure the OAuth consent screen (explicit user approval)
&lt;/li&gt;
&lt;li&gt;Create OAuth credentials (application identity)
&lt;/li&gt;
&lt;li&gt;Authenticate using custom credentials (organization-specific login)
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For personal accounts, steps 1–3 are automatically handled by Google, and step 4 passes with default credentials.  &lt;/p&gt;




&lt;h3&gt;
  
  
  1. Allow Apps Script API in the Admin Console
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grants organization-level permission to use Apps Script
&lt;/li&gt;
&lt;li&gt;Without this, organizational policy blocks access
&lt;/li&gt;
&lt;li&gt;Analogy: like unlocking the company’s front door
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steps&lt;/strong&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to the &lt;strong&gt;Admin Console&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Security&lt;/strong&gt; → &lt;strong&gt;API Controls&lt;/strong&gt; → &lt;strong&gt;App Access Control&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Either set clasp to &lt;strong&gt;Unrestricted&lt;/strong&gt;, or set to &lt;strong&gt;Restricted&lt;/strong&gt; and add clasp as a &lt;strong&gt;Trusted app&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgb4ooke68w0h0pvmuf5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgb4ooke68w0h0pvmuf5o.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Enable Apps Script API in Google Cloud Console
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The “Apps Script API” must be enabled, otherwise authentication will not proceed
&lt;/li&gt;
&lt;li&gt;By default, it may be turned off
&lt;/li&gt;
&lt;li&gt;Analogy: like switching on the building’s access system before issuing employee badges
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steps&lt;/strong&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to the &lt;strong&gt;Google Cloud Console&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;APIs &amp;amp; Services&lt;/strong&gt; → &lt;strong&gt;Library&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;Google Apps Script API&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3j9t20iyu14kkqdyi4ap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3j9t20iyu14kkqdyi4ap.png" alt=" " width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configure the OAuth Consent Screen
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is the confirmation screen that asks: “clasp wants to access your Apps Script projects. Do you allow it?”
&lt;/li&gt;
&lt;li&gt;Clearly shows what data will be accessed
&lt;/li&gt;
&lt;li&gt;Analogy: like a receptionist asking visitors about their purpose before granting entry
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steps&lt;/strong&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In &lt;strong&gt;Google Cloud Console&lt;/strong&gt;, go to &lt;strong&gt;OAuth consent screen&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter app information (name, support email)
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiuoc8ydjagyt4wx9jwps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiuoc8ydjagyt4wx9jwps.png" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set &lt;strong&gt;User Type&lt;/strong&gt; to &lt;strong&gt;Internal&lt;/strong&gt; (only users in your Workspace domain can use it)
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41kwmxh3ipv3gewak1aj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41kwmxh3ipv3gewak1aj.png" alt=" " width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add your own account as a test user
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. Create OAuth 2.0 Credentials
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allows clasp to identify itself with a Client ID and Client Secret
&lt;/li&gt;
&lt;li&gt;Analogy: like issuing an official employee ID badge
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steps&lt;/strong&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to the &lt;strong&gt;Google Cloud Console&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;APIs &amp;amp; Services&lt;/strong&gt; → &lt;strong&gt;Credentials&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xwlwwljyhnhflhfdrqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xwlwwljyhnhflhfdrqj.png" alt=" " width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an &lt;strong&gt;OAuth Client ID&lt;/strong&gt; with type &lt;strong&gt;Desktop application&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Obtain the &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frkuu5qosrn0xqohd0gqi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frkuu5qosrn0xqohd0gqi.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Authentication with Custom Credentials&amp;nbsp;&amp;nbsp;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;&amp;nbsp;&amp;nbsp;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses customized credentials for the organization instead of default, generic authentication.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Accesses resources in a manner that complies with the organization's security policy.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Example: Similar to using a dedicated employee ID card to enter a building instead of a generic visitor's badge.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Procedure&lt;/strong&gt;&amp;nbsp;&amp;nbsp;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Log out from the existing authentication.&lt;br&gt;
&lt;br&gt;
&amp;nbsp;&amp;nbsp;&lt;br&gt;
&amp;nbsp; &amp;nbsp;&lt;code&gt;bash&lt;br&gt;
&amp;nbsp; &amp;nbsp;clasp logout&lt;br&gt;
&amp;nbsp; &amp;nbsp;&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Log in using the downloaded credentials.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&amp;nbsp; &amp;nbsp;&lt;code&gt;bash&lt;br&gt;
&amp;nbsp; &amp;nbsp;clasp login --creds ./credentials.json&lt;br&gt;
&amp;nbsp; &amp;nbsp;&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Important Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Both Configurations Required&lt;/strong&gt;: Configuration is needed in both the Cloud Console and the Admin Console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time for Settings to Take Effect&lt;/strong&gt;: It may take anywhere from a few minutes to several hours for the settings to be reflected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop application is Key&lt;/strong&gt;: Web application is not sufficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal User Type&lt;/strong&gt;: The Internal user type is sufficient (for use only within the organization).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In my case, since I had administrator rights for the organization, I was able to resolve the issue relatively smoothly.&lt;br&gt;&lt;br&gt;
However, if I had been a regular user, asking the admin to investigate and resolve the cause would have been much more troublesome.  &lt;/p&gt;

&lt;p&gt;Complex configuration is unavoidable in order to meet organizational security requirements.&lt;br&gt;&lt;br&gt;
But once you understand the background, the steps make sense.&lt;br&gt;&lt;br&gt;
I hope this article will serve as a helpful reference for anyone facing the same issue. &lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Notes
&lt;/h2&gt;

&lt;p&gt;If you can successfully log in or clone using Clasp but push operations fail, it means the API isn't enabled for the Google account's App Script service settings. Please change the API usage to On via the link below. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://script.google.com/home/usersettings" rel="noopener noreferrer"&gt;https://script.google.com/home/usersettings&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfcu0r5j8z3cxj0d8zh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfcu0r5j8z3cxj0d8zh4.png" alt="setting" width="800" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/google/clasp" rel="noopener noreferrer"&gt;Google Apps Script CLI (clasp)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://admin.google.com/" rel="noopener noreferrer"&gt;Google Admin Console&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ritou.hatenablog.com/entry/2020/12/01/000000" rel="noopener noreferrer"&gt;What is OAuth Authentication? (2020) *JAPANESE&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gas</category>
      <category>tutorial</category>
      <category>security</category>
    </item>
    <item>
      <title>Thinking About Why We Build Dashboards with GAS📊</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Fri, 15 Aug 2025 15:24:18 +0000</pubDate>
      <link>https://dev.to/yo-shi/how-our-google-drive-practices-were-walking-a-dangerous-line-2d92</link>
      <guid>https://dev.to/yo-shi/how-our-google-drive-practices-were-walking-a-dangerous-line-2d92</guid>
      <description>&lt;p&gt;For one of my projects, I was asked to submit a portfolio sample, so I built a dashboard using Google Apps Script (GAS).&lt;br&gt;&lt;br&gt;
Here’s what it looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2lsgffef262xjfi7jo8l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2lsgffef262xjfi7jo8l.png" alt="dashboard image" width="509" height="861"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re curious, you can check out the live version here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://script.google.com/macros/s/AKfycbwgx15RKcusxPJk8s8FvsXQ1BvIeQlMOnoUKBfyjr88Ms2coQtdDyG6D_gVGGQmq5ci/exec" rel="noopener noreferrer"&gt;GAS_Dashboard_SAMPLE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/y-oyaizu/tech-blog-examples/tree/main/DashboardSample_using_GAS" rel="noopener noreferrer"&gt;GitHub Sample Code: DashboardSample_using_GAS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have experience building dashboards with BI tools and lightweight web apps using Python + Flask, but this was my first time building one with GAS.&lt;br&gt;&lt;br&gt;
Since this is a relatively niche use case, I thought I’d share my findings.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Does It Make Sense to Build a Dashboard with GAS?
&lt;/h2&gt;

&lt;p&gt;Google Sheets already offers charting capabilities, and BI tools can make dashboard creation quite straightforward.&lt;br&gt;&lt;br&gt;
Still, there are situations where using GAS makes sense, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding functionality not available in standard features
&lt;/li&gt;
&lt;li&gt;Creating interactive UI components
&lt;/li&gt;
&lt;li&gt;Integrating with other Google services
&lt;/li&gt;
&lt;li&gt;Managing the dashboard in code form
&lt;/li&gt;
&lt;li&gt;Keeping costs low
&lt;/li&gt;
&lt;li&gt;Fetching data from external services
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: &lt;strong&gt;a fully automated, customizable dashboard that updates itself, without the licensing cost of BI tools&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Spreadsheets and BI tools trade off flexibility for ease of use, and dashboard projects often balance customization against development/maintenance costs.&lt;/p&gt;

&lt;p&gt;Here’s my personal “feature matrix” comparing some common options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Google Sheets&lt;/th&gt;
&lt;th&gt;BI Tools&lt;/th&gt;
&lt;th&gt;GAS&lt;/th&gt;
&lt;th&gt;Scratch Development&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development Barrier&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customizability&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Volume Handling&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development Speed&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Additional Cost&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance Load&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For small organizations such as startups or SMEs, where the dataset isn’t huge, there’s no budget for BI tools or custom apps, and spreadsheet-based manual management is also impractical, GAS dashboards can be a viable middle ground.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dashboard Design Choices
&lt;/h2&gt;

&lt;p&gt;For this project, I built the dashboard as a web app and used Plotly for rendering.&lt;br&gt;&lt;br&gt;
Why Plotly? I was already comfortable with it from data analysis projects, and since this was a sample, I wanted to highlight interactivity and responsiveness.&lt;/p&gt;

&lt;p&gt;That said, Plotly isn’t the most common choice for GAS dashboards.&lt;br&gt;&lt;br&gt;
If you’re building a web app dashboard with GAS, the more typical approach is &lt;strong&gt;Google Charts Service (Charts API)&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
It’s built-in, renders on the server side (reducing browser load), requires no extra libraries, and is straightforward to use with methods like &lt;code&gt;Charts.newAreaChart()&lt;/code&gt; or &lt;code&gt;Charts.newLineChart()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Plotly, by contrast, is loaded via CDN through the HTML Service and rendered in the browser.&lt;br&gt;&lt;br&gt;
It offers rich features—hover tooltips, animations, responsive layouts—out of the box, but it requires HTML/CSS/JavaScript knowledge and more complex setup.&lt;br&gt;&lt;br&gt;
Heavy datasets can cause browser freezes.&lt;/p&gt;

&lt;p&gt;Also, remember GAS’s limitations: a maximum execution time of 6 minutes and limited memory.&lt;br&gt;&lt;br&gt;
With datasets in the 100k+ range, it’s more practical to preprocess with BigQuery or switch to a BI tool.&lt;/p&gt;

&lt;p&gt;In my case, even with ~2,000 rows, I noticed a few seconds of lag during aggregation. It’s borderline acceptable but not ideal UX. For a sample, I decided that was fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Sample Data
&lt;/h2&gt;

&lt;p&gt;At first, I looked for government datasets, but I needed something that wasn’t too large yet still rich enough for graphing. Overly simple datasets make poor demos, and I didn’t want to spend too much time on data preparation.&lt;/p&gt;

&lt;p&gt;I ended up using Kaggle’s &lt;a href="https://www.kaggle.com/datasets/pratyushpuri/retail-fashion-boutique-data-sales-analytics-2025" rel="noopener noreferrer"&gt;Retail Fashion Boutique Data Sales Analytics 2025&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
Kaggle datasets are well-documented, tagged, and even provide quick visual previews—very convenient.&lt;br&gt;&lt;br&gt;
However, as is often the case with competition datasets, the data can be biased; here, many records were clustered in a specific month. A quick exploratory check beforehand is always a good idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do You Even Need a Dashboard?
&lt;/h2&gt;

&lt;p&gt;This might sound counterintuitive, but before building a dashboard, you should first ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who will use it?
&lt;/li&gt;
&lt;li&gt;What information will lead to actionable insights?
&lt;/li&gt;
&lt;li&gt;Why did “dashboard” become the chosen solution?
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without answering these, you risk creating something no one uses but still requires ongoing maintenance.&lt;/p&gt;

&lt;p&gt;In many cases, a dashboard just centralizes metrics that are already calculated elsewhere.&lt;br&gt;&lt;br&gt;
If the goal is simply to reduce reporting workload, a plain text summary could be cheaper and faster.&lt;/p&gt;

&lt;p&gt;And when you aggregate metrics, they often become more abstract, meaning you still have to drill down into the raw data.&lt;br&gt;&lt;br&gt;
That’s why I prefer to see dashboards as &lt;strong&gt;portal sites to distributed datasets&lt;/strong&gt;, rather than as the final stop for analysis.&lt;/p&gt;

&lt;p&gt;That said, dashboards can feel fresh and engaging for users, and they can automate repetitive reporting tasks.&lt;br&gt;&lt;br&gt;
The key for developers is to maintain a critical eye: is this really the best solution for the problem at hand?&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/apps-script/reference/charts" rel="noopener noreferrer"&gt;Google Workspace &amp;gt; Apps Script &amp;gt; Charts Service&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@eriktuck/build-a-time-tracking-app-with-google-apps-script-and-plotly-43c776f5eb80" rel="noopener noreferrer"&gt;Use the Plotly JavaScript library to create data visualizations (part 3 of 3)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://data.gov/" rel="noopener noreferrer"&gt;DATA.GOV&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kaggle.com/datasets" rel="noopener noreferrer"&gt;Kaggle - Datasets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gas</category>
      <category>automation</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How Our Google Drive Practices Were Walking a Dangerous Line</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Wed, 11 Jun 2025 00:49:36 +0000</pubDate>
      <link>https://dev.to/yo-shi/how-our-google-drive-practices-were-walking-a-dangerous-line-20dd</link>
      <guid>https://dev.to/yo-shi/how-our-google-drive-practices-were-walking-a-dangerous-line-20dd</guid>
      <description>&lt;p&gt;Hey everyone, are you using Google Drive?&lt;br&gt;&lt;br&gt;
It’s one of the core services in Google Workspace, right alongside Gmail and Google Calendar.  &lt;/p&gt;

&lt;p&gt;Google Workspace is built for everyone—from IT pros to everyday users—so the user interface (UI) is designed to be intuitive and easy to use. I’ve personally been using Google Drive for over ten years without anyone ever teaching me how.&lt;/p&gt;

&lt;p&gt;But when I took on the role of a Google Workspace Admin, I realized something troubling:&lt;br&gt;&lt;br&gt;
The very features that make Google Drive "easy to use" were also the source of a lot of potential issues.&lt;/p&gt;

&lt;p&gt;Recently, I had a chance to investigate and organize our team's use of Google Drive.&lt;br&gt;&lt;br&gt;
This article shares what I learned—especially for non-IT folks—about what can easily go wrong and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  So, What Is Google Workspace Anyway?
&lt;/h2&gt;

&lt;p&gt;Chances are, you already have a personal Google account.&lt;br&gt;&lt;br&gt;
Once you have one, you get access to many powerful tools: Gmail, Google Calendar, Google Maps, Google Docs, Google Meet—you name it.  &lt;/p&gt;

&lt;p&gt;According to Gemini, as of May 2025, Gmail alone has over 1.8 billion active users.&lt;/p&gt;

&lt;p&gt;Google Workspace takes these tools and packages them for business use.&lt;br&gt;&lt;br&gt;
It allows organizations to manage things centrally—like using custom domains, setting up user accounts, and controlling security policies.&lt;/p&gt;

&lt;p&gt;In short, it’s like renting a fully-equipped virtual office.&lt;br&gt;&lt;br&gt;
If you compare it to a physical office, everything starts to make more sense.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;In a Physical Office&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom Domain&lt;/td&gt;
&lt;td&gt;Set up emails like &lt;a href="mailto:user@yourdomain.com"&gt;user@yourdomain.com&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Company nameplate or office signage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gmail&lt;/td&gt;
&lt;td&gt;Send and receive emails&lt;/td&gt;
&lt;td&gt;Your desk mailbox (internal and external messages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drive&lt;/td&gt;
&lt;td&gt;Store and share files&lt;/td&gt;
&lt;td&gt;Filing cabinet for documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meet&lt;/td&gt;
&lt;td&gt;Online meetings&lt;/td&gt;
&lt;td&gt;Conference room or phone meeting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar&lt;/td&gt;
&lt;td&gt;Schedule events and meetings&lt;/td&gt;
&lt;td&gt;Room booking board or whiteboard planner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groups&lt;/td&gt;
&lt;td&gt;Mailing lists&lt;/td&gt;
&lt;td&gt;Department contact list or circular board&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;Create documents&lt;/td&gt;
&lt;td&gt;Meeting notes on a whiteboard or shared notebook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat&lt;/td&gt;
&lt;td&gt;Team text communication&lt;/td&gt;
&lt;td&gt;Hallway chats, memos, or sticky notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Management&lt;/td&gt;
&lt;td&gt;Create/delete accounts, assign permissions&lt;/td&gt;
&lt;td&gt;Staff roster or building access management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin Console&lt;/td&gt;
&lt;td&gt;Policy settings, security, audit logs&lt;/td&gt;
&lt;td&gt;Security system and master key control panel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With Google Workspace, your organization gets all essential office functions—bundled in one place.&lt;br&gt;&lt;br&gt;
It’s super convenient and designed to make management smoother.&lt;/p&gt;




&lt;h2&gt;
  
  
  So, Where Do You Store Your Files?
&lt;/h2&gt;

&lt;p&gt;Now, let’s focus on today’s topic: Google Drive.&lt;br&gt;&lt;br&gt;
Think of it as your digital filing cabinet. When you have lots of documents, you need a good place to organize them.&lt;/p&gt;

&lt;p&gt;Here’s the catch: there are actually two kinds of cabinets—one for individuals and one for the organization.&lt;br&gt;&lt;br&gt;
The personal one is called &lt;strong&gt;My Drive&lt;/strong&gt;, and the shared one is called &lt;strong&gt;Shared Drives&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’re using a regular Google account (not a Workspace account), you won’t see Shared Drives.&lt;br&gt;&lt;br&gt;
It’s a feature unique to Google Workspace.&lt;/p&gt;

&lt;p&gt;To use a real office analogy:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared Drives&lt;/strong&gt; are like open shelves in a common room that everyone can access.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My Drive&lt;/strong&gt; is like the drawer next to your own desk—your personal storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0o9tmyromhppn3l5fqn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0o9tmyromhppn3l5fqn.png" alt="2 drives" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Drive&lt;/strong&gt; is basically your private space.&lt;br&gt;&lt;br&gt;
If you want to share files, you need to manually choose who can access them—specific users or groups.&lt;/p&gt;

&lt;p&gt;Even administrators can’t just peek into someone’s My Drive.&lt;br&gt;&lt;br&gt;
They’d need special tools and a good reason (like an audit).&lt;br&gt;&lt;br&gt;
It’s like having a locked drawer at your desk.&lt;br&gt;&lt;br&gt;
Even your boss wouldn’t open it without a master key—and that’s only for serious cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared Drives&lt;/strong&gt;, on the other hand, also have access controls—based on &lt;strong&gt;roles&lt;/strong&gt; and &lt;strong&gt;targets&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Roles
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Viewer&lt;/strong&gt;: Can only view (download/print can be restricted)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commenter&lt;/strong&gt;: Can comment but not edit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editor&lt;/strong&gt;: Can edit, reshare, and delete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Manager&lt;/strong&gt;: Can edit and organize within the Shared Drive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manager&lt;/strong&gt;: Full control, including member management&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Targets
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Specific users (via email)&lt;/li&gt;
&lt;li&gt;Google Groups (like &lt;code&gt;team@yourdomain.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;All users in the domain (anyone with &lt;code&gt;@yourdomain.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Anyone with the link (higher security risk)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Actually, both My Drive and Shared Drives allow you to &lt;strong&gt;store and share&lt;/strong&gt; files.&lt;br&gt;&lt;br&gt;
But the &lt;strong&gt;flow of control&lt;/strong&gt; is different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: Store in My Drive&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share only when needed
&lt;/li&gt;
&lt;li&gt;Starts narrow → expands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pattern 2: Store in Shared Drive&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict access only when needed
&lt;/li&gt;
&lt;li&gt;Starts wide → narrows down&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Big Difference: File Ownership
&lt;/h2&gt;

&lt;p&gt;Here’s the key: &lt;strong&gt;Who owns the file?&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files in &lt;strong&gt;My Drive&lt;/strong&gt; are owned by the &lt;strong&gt;individual&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Files in &lt;strong&gt;Shared Drives&lt;/strong&gt; are owned by the &lt;strong&gt;organization&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a user leaves the company and their account is deleted,&lt;br&gt;&lt;br&gt;
everything in their My Drive disappears—even shared files.&lt;br&gt;&lt;br&gt;
It’s like tossing out a former employee’s entire desk.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F49exfkb5kyy586bdbn1u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F49exfkb5kyy586bdbn1u.png" alt="the desk is gone" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the other hand, files in Shared Drives remain safe—even when users leave the organization.&lt;/p&gt;

&lt;p&gt;This kind of structure makes file ownership and collaboration clearer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-progress work&lt;/strong&gt; and &lt;strong&gt;personal to-do lists&lt;/strong&gt; → store in &lt;strong&gt;My Drive&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files meant to be shared with your team or org&lt;/strong&gt; → store in &lt;strong&gt;Shared Drives&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds simple, right?&lt;/p&gt;

&lt;p&gt;But in real-world usage, things often get messy.&lt;br&gt;&lt;br&gt;
Another Google Drive feature—the “Shared with me” list—often leads things in the wrong direction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with “Shared with Me”
&lt;/h2&gt;

&lt;p&gt;The “Shared with me” section exists in both personal and Workspace accounts.&lt;br&gt;&lt;br&gt;
It shows a list of files and folders others have shared with you.&lt;/p&gt;

&lt;p&gt;For personal accounts, it kind of makes sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your stuff = My Drive
&lt;/li&gt;
&lt;li&gt;Other people’s stuff = Shared with me&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here’s the catch:&lt;br&gt;&lt;br&gt;
This list is &lt;strong&gt;just a notification box&lt;/strong&gt; showing newly shared links.&lt;br&gt;&lt;br&gt;
There’s no folder structure, no organization—just a time-ordered feed.&lt;br&gt;&lt;br&gt;
And you can’t rename, reorder, or delete anything from it.&lt;/p&gt;

&lt;p&gt;So what happens?&lt;/p&gt;

&lt;p&gt;People want control.&lt;br&gt;&lt;br&gt;
They want to tidy things up.&lt;/p&gt;

&lt;p&gt;So, naturally, they right-click the file...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fot0z1bmxippoensn134k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fot0z1bmxippoensn134k.png" alt="view" width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;...and then a menu appears:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Download&lt;/strong&gt;, &lt;strong&gt;Make a copy&lt;/strong&gt;, &lt;strong&gt;Add shortcut&lt;/strong&gt;, and &lt;strong&gt;Add to starred&lt;/strong&gt;—all seemingly useful for organizing files.&lt;/p&gt;

&lt;p&gt;Here’s what each does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Relation to Original File&lt;/th&gt;
&lt;th&gt;Organizing Style&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Download&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Saves a copy to your computer. Good for offline use or archiving.&lt;/td&gt;
&lt;td&gt;No longer connected&lt;/td&gt;
&lt;td&gt;Local storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Make a copy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Saves a new file to your My Drive. Freely editable and shareable.&lt;/td&gt;
&lt;td&gt;No longer connected&lt;/td&gt;
&lt;td&gt;Personal reuse or resharing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Add shortcut&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adds a link to the original file in your My Drive. Can be placed in folders.&lt;/td&gt;
&lt;td&gt;Still linked&lt;/td&gt;
&lt;td&gt;Custom folder organization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Add to starred&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Marks the file with a star for quick access. Flat list, no folder structure.&lt;/td&gt;
&lt;td&gt;Still linked&lt;/td&gt;
&lt;td&gt;Bookmark-style, searchable view&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Why This Matters for Admins
&lt;/h2&gt;

&lt;p&gt;From an admin’s perspective, when users want to &lt;strong&gt;organize shared files&lt;/strong&gt;, the best practice is:&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;“Add shortcut”&lt;/strong&gt; or &lt;strong&gt;“Add to starred”&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
These keep the file structure intact and avoid unnecessary duplicates.&lt;/p&gt;

&lt;p&gt;But here’s the catch...&lt;br&gt;&lt;br&gt;
These two options don’t appear right away in the right-click menu.&lt;br&gt;&lt;br&gt;
To find them, users must go deeper—through the &lt;strong&gt;“Organize”&lt;/strong&gt; submenu.&lt;/p&gt;

&lt;p&gt;As a result, people often just choose &lt;strong&gt;“Make a copy”&lt;/strong&gt; or &lt;strong&gt;“Download”&lt;/strong&gt;,&lt;br&gt;&lt;br&gt;
which breaks the connection to the original and creates version chaos.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5fnbwtmo22hv3rtegjb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5fnbwtmo22hv3rtegjb.png" alt="why" width="800" height="595"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because these options aren’t shown clearly or intuitively, many users end up clicking “Make a copy” by mistake.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;p&gt;You end up with dozens of files like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“Copy of…”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;“Copy of Copy of…”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This breaks the benefits of cloud collaboration:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There’s no single source of truth
&lt;/li&gt;
&lt;li&gt;Files get out of sync
&lt;/li&gt;
&lt;li&gt;People don’t know which version is the latest&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Strange Case of Shared Accounts
&lt;/h2&gt;

&lt;p&gt;In our organization, we used to share the &lt;strong&gt;My Drive&lt;/strong&gt; of a special account called &lt;code&gt;SharedDrive@...&lt;/code&gt; as a workaround for Shared Drives.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Before March 2017, &lt;strong&gt;Shared Drives&lt;/strong&gt; didn’t exist.&lt;br&gt;&lt;br&gt;
The only way to share files across a team was to use someone’s My Drive as a shared hub.&lt;/p&gt;

&lt;p&gt;This workaround had some serious problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the account was deleted, &lt;strong&gt;all files disappeared&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;File ownership was personal, not organizational&lt;/li&gt;
&lt;li&gt;Centralized control by admins was nearly impossible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the time, it was a practical—but imperfect—solution.&lt;br&gt;
But today, &lt;strong&gt;Shared Drives&lt;/strong&gt; are the much safer and more manageable solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended Best Practices
&lt;/h2&gt;

&lt;p&gt;Google Drive is incredibly useful,&lt;br&gt;&lt;br&gt;
but if used carelessly, it can cause:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lost files
&lt;/li&gt;
&lt;li&gt;Confusion about ownership
&lt;/li&gt;
&lt;li&gt;Bad handovers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As admins, here’s how we define each space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My Drive&lt;/strong&gt; → for temporary personal work
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared Drives&lt;/strong&gt; → for official, shared documents
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared with me&lt;/strong&gt; → just a notification list, not a workspace&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Our Organizational Guidelines
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Store important files in Shared Drives&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
For any document meant to be reused by the team.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use My Drive as a personal sandbox&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Drafts, notes, and temporary files only.&lt;br&gt;&lt;br&gt;
Move finished work to a Shared Drive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don’t rely too much on “Shared with me”&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use &lt;strong&gt;shortcuts&lt;/strong&gt; or &lt;strong&gt;stars&lt;/strong&gt; to organize what you need.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use stars for quick access&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Especially useful when you don’t need folder structure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If your organization has experienced any of these issues,&lt;br&gt;&lt;br&gt;
I strongly recommend shifting to &lt;strong&gt;Shared Drives&lt;/strong&gt; and setting clear usage rules.&lt;/p&gt;

&lt;p&gt;Yes, it takes some effort at first—but in the long run,&lt;br&gt;&lt;br&gt;
you’ll gain clarity, control, and peace of mind in your file management.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://techcrunch.com/2016/11/21/google-opens-up-its-new-product-for-business-file-sharing-team-drives-to-early-adopters/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Google opens up its new product for business file sharing “Team Drives” to early adopters&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://workspaceupdates.googleblog.com/2017/03/introducing-new-enterprise-ready-tools-google-drive.html?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Google Workspace Updates&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tettra.com/article/organize-google-drive/" rel="noopener noreferrer"&gt;Organizing Google Drive: 13 Best Tips for Your Business&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>google</category>
      <category>beginners</category>
      <category>cloud</category>
    </item>
    <item>
      <title>DIY Ticketing System with Google Apps Script for Handling Inquiries</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Wed, 21 May 2025 14:58:39 +0000</pubDate>
      <link>https://dev.to/yo-shi/diy-ticketing-system-with-google-apps-script-for-handling-inquiries-2ed4</link>
      <guid>https://dev.to/yo-shi/diy-ticketing-system-with-google-apps-script-for-handling-inquiries-2ed4</guid>
      <description>&lt;h2&gt;
  
  
  Started Volunteering as a System Admin for an NPO
&lt;/h2&gt;

&lt;p&gt;About two months ago, I started volunteering for an NPO in a system admin role. The organization is quite pre-modern in how it operates—user inquiries were handled entirely via email. Sometimes our IT-related team handled the issues, and other times we had to forward them to the appropriate department. Of course, that meant forwarding the emails. I hadn’t seen dozens of endless “Re:Re:Re:” threads in a while. It felt like time-traveling back to the 2000s when I first entered the workforce.&lt;/p&gt;

&lt;p&gt;Fortunately, the NPO uses Google Workspace for Nonprofits, so we have access to the basic Google apps. I decided to centralize user inquiries using a Google Form. However, form responses are aggregated into a spreadsheet by default, and managing each case individually that way is clearly a pain. At the same time, it didn’t seem worth it to introduce full-fledged ticketing systems like JIRA or Salesforce. So, I built a simple ticket system using Google Apps Script (GAS).&lt;/p&gt;

&lt;p&gt;I'm sharing the system structure and the lessons I learned here as a record. I hope it’s helpful for non-engineers, NPO staff, or small business operators who can’t spend much on tools.&lt;/p&gt;

&lt;p&gt;(Update: Sample code is available here → &lt;a href="https://github.com/y-oyaizu/tech-blog-examples/tree/main/Helpdesk_Tools" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  System Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Since we're limited to Google Workspace, the setup is very simple. After designing the overall structure and creating a UI mockup in HTML, I had ChatGPT generate the implementation code, then manually tweaked the finer details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Form
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Collects user inquiries
&lt;/li&gt;
&lt;li&gt;Accessible to anyone with the link&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Spreadsheet
&lt;/h3&gt;

&lt;p&gt;The spreadsheet is split into two files based on purpose:&lt;/p&gt;

&lt;h4&gt;
  
  
  Sheet(1): FormResponses
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Auto-generated when collecting form data
&lt;/li&gt;
&lt;li&gt;Script is triggered on each new entry
&lt;/li&gt;
&lt;li&gt;Issues a Unique ID (UID)
&lt;/li&gt;
&lt;li&gt;Copies the data into the ticket log sheet (TicketLogs)
&lt;/li&gt;
&lt;li&gt;Sends UID to the user via email
&lt;/li&gt;
&lt;li&gt;Also posts to a Google Chat thread for the support team&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Sheet(2): TicketLogs
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Serves as a pseudo-database
&lt;/li&gt;
&lt;li&gt;Not accessible to non-admins
&lt;/li&gt;
&lt;li&gt;Edits are appended to the bottom&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Apps Script
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Provides a UI to view and respond to inquiries
&lt;/li&gt;
&lt;li&gt;Only shows the latest row of data from the logs
&lt;/li&gt;
&lt;li&gt;Access restricted to users within the organization domain&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  System Diagram
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj43js5891pdy3ws5gmz7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj43js5891pdy3ws5gmz7.png" alt="DataFlow" width="681" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Web App Functions and Process Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  UI Screenshots
&lt;/h3&gt;

&lt;p&gt;Here’s a screenshot of the ticket management screen. Basic filtering is implemented.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fajgj6o4reori6bfh24mo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fajgj6o4reori6bfh24mo.png" alt="TicketUI" width="800" height="826"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking the "Edit" button opens a modal where you can edit individual tickets.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fil0t3euv1ajzr84tmfl8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fil0t3euv1ajzr84tmfl8.png" alt="Modal" width="714" height="923"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Code.gs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Code.gs&lt;/code&gt; handles the backend, retrieving data from the spreadsheet.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doGet()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns the UI as a web app. Entry point that displays the HTML page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onFormSubmit(e)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Triggered when a Google Form is submitted. Issues a ticket ID, logs it, sends an email, and posts to Google Chat.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sendToGoogleChat(ticketId, name, category, inquiry)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Notifies Google Chat about new tickets using a webhook.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getTickets()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieves only the latest state for each ticket from the log sheet. Used to display the list in the UI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updateTicket(updateData)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Appends updated info to the log sheet based on the given ticket data (no overwrite, keeps history).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Ticket Screen (HTML + JS) Functions and Overview
&lt;/h3&gt;

&lt;p&gt;The ticket management screen is displayed via Google Apps Script’s &lt;code&gt;HtmlService&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
Built using HTML (Bootstrap + FontAwesome) and JavaScript to create the UI and interact with GAS.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function / Element Name&lt;/th&gt;
&lt;th&gt;Role / Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loadTickets(isRefresh)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Calls &lt;code&gt;getTickets()&lt;/code&gt; to fetch data, then runs &lt;code&gt;applyFilters()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;applyFilters(initialLoad)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filters the ticket list by number, category, status, and assignee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renderTickets(tickets)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Converts the ticket list into HTML and outputs it to &lt;code&gt;#ticketList&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openEditModal(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Populates the modal with ticket data and displays it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;saveModalTicket()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gets data from modal, calls &lt;code&gt;updateTicket()&lt;/code&gt;, closes modal, refreshes UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;escapeHtml(str)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Escapes HTML to prevent XSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DOMContentLoaded&lt;/code&gt; event&lt;/td&gt;
&lt;td&gt;Initialization: sets up event listeners and loads open tickets on first load&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  HTML Structure Elements
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Selector / Element&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.filters&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filter UI: ticket number, category, status, assignee, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#ticketList&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Area to display the ticket list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.modal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Edit modal (built using Bootstrap Modal)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Web App with GAS
&lt;/h3&gt;

&lt;p&gt;What this GAS project does is display and manage spreadsheet data through a custom UI. While similar operations can be done directly in a spreadsheet, it's not convenient to record and edit everything in one sheet. When history is written directly into spreadsheet cells, inputs become messy, and it’s unclear whether to overwrite or append—leading to inconsistent user behavior. By providing a unified form, it becomes easier to trace past actions even if responsibilities are handed over to different team members.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spreadsheet Operation
&lt;/h3&gt;

&lt;p&gt;By separating the input sheet and the log sheet, mixed data is avoided. With a dedicated log sheet, admins can trace the edit history. Non-admins are denied access to the base data, reducing the risk of tampering. Ideally, version control-style diffs would have been great, but that felt like overkill and was left out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Aggregation / Standardization
&lt;/h3&gt;

&lt;p&gt;By centralizing the inquiry channel and standardizing the data format, we should be able to accumulate training data for a future FAQ chatbot. Current FAQs are based mostly on human intuition, so there’s definitely room for improvement using actual data.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI-Based Implementation
&lt;/h3&gt;

&lt;p&gt;Starting simple and refining based on clearly identified issues is the way to go. One bitter memory: the initial ChatGPT code was too simple, so I asked Claude to review it—only for it to completely rewrite things and break the implementation beyond repair. Since I had little front-end experience, I kept things simple and upgraded step by step. As a result, I picked up the knack for making precise fixes and tracking down bugs early on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While focusing on design and review, the core implementation was completed in just a few hours. The ability to release only what you need at low cost is a true advantage of building things in-house.&lt;/p&gt;

&lt;p&gt;That said, “maintenance” is now the next challenge. Can non-engineers maintain such a system with AI support? Despite the hype that AI can solve everything, as long as systems interact with people and organizations, a minimum level of technical literacy is still necessary to stay accountable—and to avoid building the wrong thing entirely. It’s a tough problem. Perhaps we’ll need a system where AI interviews the current admin to gather maintenance context. But then again—who will maintain that system? The recursion never ends.&lt;/p&gt;

</description>
      <category>gas</category>
      <category>beginners</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How the Liskov Substitution Principle Clarifies Delegation in Organizations</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Tue, 15 Apr 2025 01:17:50 +0000</pubDate>
      <link>https://dev.to/yo-shi/how-the-liskov-substitution-principle-clarifies-delegation-in-organizations-10n8</link>
      <guid>https://dev.to/yo-shi/how-the-liskov-substitution-principle-clarifies-delegation-in-organizations-10n8</guid>
      <description>&lt;p&gt;"You can safely leave it to this person"—this is a phrase often heard in business settings. But in reality, it's not uncommon for problems to arise after a handover.&lt;/p&gt;

&lt;p&gt;I once experienced this myself when I took over a client account from my manager. I was explicitly told to act autonomously, so I made decisions based on what I thought was best given the client’s situation. In doing so, I ended up modifying the clear guidelines passed down from my manager. For example, while working in corporate sales proposing system solutions to various companies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I was supposed to follow up on proposal progress every week, but I waited two weeks thinking “the client seems busy”&lt;/li&gt;
&lt;li&gt;I failed to push back on some “nice-to-have” requirements and brought them back to the team&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These decisions backfired. The process took too long, yielded no results, and the deal didn’t close. My manager gave me direct feedback: “You shouldn't change the criteria on your own.” That’s when I realized I had misunderstood what it really meant to “act autonomously.”&lt;/p&gt;

&lt;p&gt;This experience resonated deeply with a software design principle I later learned—the &lt;em&gt;Liskov Substitution Principle&lt;/em&gt; (LSP). LSP states that a subclass should be substitutable for its superclass without altering the correctness of the program.&lt;/p&gt;

&lt;p&gt;In a business context, if a subordinate is to act in place of a superior, it’s essential to clearly identify the &lt;strong&gt;preconditions&lt;/strong&gt;, &lt;strong&gt;postconditions&lt;/strong&gt;, and &lt;strong&gt;invariants&lt;/strong&gt;—what must be preserved and what can be improved. I made decisions without clarifying those boundaries.&lt;/p&gt;

&lt;p&gt;In software, when a subclass extends a superclass without breaking its behavioral contract, the system remains stable. The same applies to organizations: when roles are handed down from managers to subordinates, if expectations, deliverables, and standards aren't preserved, trust and results across the team can collapse.&lt;/p&gt;

&lt;p&gt;LSP, therefore, is not just a principle for software design—it also offers practical insight into how we structure role transitions and delegation within organizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the LSP?
&lt;/h2&gt;

&lt;p&gt;Many of you may already be familiar with it, but the Liskov Substitution Principle (LSP) is an object-oriented design principle proposed by Barbara Liskov in 1987.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To uphold this principle, the following conditions are crucial. Let’s look at them through the example of an airline mileage program.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preconditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A subclass must &lt;strong&gt;require the same or weaker conditions&lt;/strong&gt; than its superclass.&lt;/li&gt;
&lt;li&gt;An operation allowed in the superclass must not be disallowed in the subclass.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Postconditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A subclass must &lt;strong&gt;guarantee the same or stronger outcomes&lt;/strong&gt; than its superclass.&lt;/li&gt;
&lt;li&gt;It’s fine for a subclass to return more than what was guaranteed by the superclass, but never less.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Invariants
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Any conditions that are always true in the superclass must remain true in the subclass.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Subclasses may strengthen the rules, but must not violate or weaken them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Concrete Design Example
&lt;/h2&gt;

&lt;p&gt;Here, we'll use a mileage service as a case study to illustrate how the three conditions of the LSP can be applied to the design of a customer class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;book_flight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_class&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;seat_class&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;economy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Precondition
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Flight booked in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;seat_class&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_mileage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# Postcondition: always grants 100 miles
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# Invariant: always remains active
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RoyalCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;book_flight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_class&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Looser precondition: allows first class bookings
&lt;/span&gt;        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;seat_class&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;economy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Royal booking in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;seat_class&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_mileage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Stronger postcondition: grants more miles
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Maintains the same invariant
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As shown, the RoyalCustomer class can be substituted for the Customer class without breaking the system—and in fact, it enhances functionality. This is an example of a design that respects the Liskov Substitution Principle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvzpzmvhp9suhwdu0pq5k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvzpzmvhp9suhwdu0pq5k.png" alt="Conditions" width="675" height="732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Preconditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Customer: Only "economy" and "business" classes can be booked
&lt;/li&gt;
&lt;li&gt;RoyalCustomer: Also allows "first" class → Loosening the precondition (✔ OK)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Postconditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Customer: Grants 100 miles
&lt;/li&gt;
&lt;li&gt;RoyalCustomer: Grants 300 miles → Enhancing the outcome (✔ OK)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Invariants
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Customer: &lt;code&gt;is_active()&lt;/code&gt; is always &lt;code&gt;True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;RoyalCustomer: Maintains &lt;code&gt;is_active()&lt;/code&gt; in the same way → Preserves the invariant (✔ OK)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  LSP-Like Behavior in the Sales Field
&lt;/h2&gt;

&lt;p&gt;The relationship between managers and their subordinates closely resembles class inheritance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preconditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Manager: Only attends meetings where decision-makers are present
&lt;/li&gt;
&lt;li&gt;Subordinate: Will visit even at the staff level
→ Broadening the scope of engagement = loosening the precondition (✔ OK)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Postconditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Manager: Presents a quote and clarifies the next action
&lt;/li&gt;
&lt;li&gt;Subordinate: Presents a quote &lt;em&gt;and&lt;/em&gt; nails down contract details for the next visit
→ Turning next steps into concrete actions = enhancing the outcome (✔ OK)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Invariants
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Manager: Always responds fairly and politely, even before a contract is signed
&lt;/li&gt;
&lt;li&gt;Subordinate: Responds to all inquiries within one business day regardless of contract status
→ Strengthening response standards while preserving the original invariant (✔ OK)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Examples of Violations (NG cases)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“I won’t take any action unless the client reaches out first.”&lt;br&gt;&lt;br&gt;
→ Making preconditions stricter (✘ Violation)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“We sent the quote, so we’re waiting to hear back.”&lt;br&gt;&lt;br&gt;
→ Weakening postconditions (✘ Violation)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Whether or not we build trust depends on the situation.”&lt;br&gt;&lt;br&gt;
→ Breaking the invariant (✘ Violation)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the LSP Applies to Organizations Too
&lt;/h2&gt;

&lt;p&gt;Software systems and business organizations share several key similarities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They operate in a hierarchical structure (e.g., parent → child, manager → subordinate)
&lt;/li&gt;
&lt;li&gt;They involve the delegation of abstract contracts and responsibilities
&lt;/li&gt;
&lt;li&gt;They are designed for efficiency based on the assumption of &lt;strong&gt;substitutability&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the principles behind software design can also be applied to how people and organizations function. Through my experience in sales—particularly the failures—I intuitively came to understand that &lt;strong&gt;freedom of action must be balanced with behavioral contracts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Liskov Substitution Principle put this into clear and formal language. It struck me deeply. This principle isn’t just about keeping systems stable—it can serve as a powerful guide for human decision-making as well.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackify.com/solid-design-liskov-substitution-principle/" rel="noopener noreferrer"&gt;SOLID Design Principles Explained: The Liskov Substitution Principle with Code Examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/tkarropoulos/demystifying-the-liskov-substitution-principle-a-guide-for-developers-3gmm"&gt;Demystifying the Liskov Substitution Principle: A Guide for Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qiita.com/yuki153/items/142d0d7a556cab787fad" rel="noopener noreferrer"&gt;リスコフの置換原則（LSP）をしっかり理解する (JAPANESE)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qiita.com/k2491p/items/d442344a462d3a574acc" rel="noopener noreferrer"&gt;【SOLID】リスコフの置換原則を完全に理解したい (JAPANESE)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>beginners</category>
      <category>management</category>
    </item>
    <item>
      <title>Understanding the Role of Decision Table Testing</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Mon, 31 Mar 2025 02:33:45 +0000</pubDate>
      <link>https://dev.to/yo-shi/understanding-the-role-of-decision-table-testing-23ep</link>
      <guid>https://dev.to/yo-shi/understanding-the-role-of-decision-table-testing-23ep</guid>
      <description>&lt;p&gt;One type of software testing is &lt;strong&gt;Decision Table Testing&lt;/strong&gt;. In real-world projects, it’s often created using Excel. Its visual clarity makes it easy to understand even for non-technical team members who have strong domain knowledge.&lt;/p&gt;

&lt;p&gt;On the other hand, when confirming the behavior of the implemented code, &lt;strong&gt;structural testing&lt;/strong&gt; (like unit tests) is commonly used.&lt;br&gt;&lt;br&gt;
(If you'd like to dive deeper into structural testing, feel free to check my previous post: &lt;a href="https://dev.to/yo-shi/understanding-software-test-coverage-criteria-step-by-step-from-line-coverage-to-mcdc-54k0"&gt;Understanding Software Test Coverage Criteria Step by Step: From Line Coverage to MC/DC&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;At first glance, these two approaches may seem completely different. However, understanding &lt;em&gt;why&lt;/em&gt; both are necessary for testing the same logic is extremely important in practice. This article explores the reasons and background behind that.&lt;/p&gt;
&lt;h2&gt;
  
  
  Basic Structure of a Decision Table
&lt;/h2&gt;

&lt;p&gt;A decision table has a very simple structure. Conditions are written vertically, with the outcome listed at the bottom. Each horizontal column represents a &lt;em&gt;variant&lt;/em&gt;—a combination of input values for each condition.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Variant 1&lt;/th&gt;
&lt;th&gt;Variant 2&lt;/th&gt;
&lt;th&gt;Variant 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Age &amp;gt; 18&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has ID&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;→ Result&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;NG&lt;/td&gt;
&lt;td&gt;NG&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conditions&lt;/strong&gt;: Assumptions or inputs used to make decisions (rows)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variants&lt;/strong&gt;: Test cases created from combinations of conditions (columns)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using this kind of table helps organize test cases in a structured and comprehensive way.&lt;br&gt;&lt;br&gt;
Just like structural testing, there are also various strategies depending on the testing purpose.&lt;/p&gt;
&lt;h3&gt;
  
  
  Test Strategy Comparison in Decision Tables
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy Name&lt;/th&gt;
&lt;th&gt;All Conditions Covered&lt;/th&gt;
&lt;th&gt;All Results Covered&lt;/th&gt;
&lt;th&gt;All Explicit Cases Covered&lt;/th&gt;
&lt;th&gt;Check Condition Effectiveness (MC/DC)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;all-conditions&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;all-decisions&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;all-explicit-variants&lt;/td&gt;
&lt;td&gt;✅ (explicit only)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MC/DC (Condition Independence)&lt;/td&gt;
&lt;td&gt;✅ (only necessary)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Why Do We Use Different Approaches?
&lt;/h2&gt;

&lt;p&gt;At this point, you might wonder:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If we confirm specifications using a decision table → implement the code based on those specs → verify behavior using structural testing...&lt;br&gt;&lt;br&gt;
then do we really need decision table-based testing?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s a fair question.&lt;/p&gt;

&lt;p&gt;However, even if we’re testing the same logic, the &lt;strong&gt;focus&lt;/strong&gt; of each approach is different — and that’s why we need both.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Suitable Testing Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prevent missing conditions or rules&lt;/td&gt;
&lt;td&gt;Model-based (Decision Table)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirm there are no implementation mistakes&lt;/td&gt;
&lt;td&gt;Structural Testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Why Is Decision Table Testing Necessary?
&lt;/h2&gt;

&lt;p&gt;Even if specifications are well-organized using a decision table and proper structural tests are conducted, decision table-based testing still brings unique value in the following ways:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Implementation Doesn’t Always Match the Specification
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;There can be &lt;strong&gt;overlooked or misunderstood requirements&lt;/strong&gt; during implementation.&lt;/li&gt;
&lt;li&gt;Decision table testing verifies the &lt;strong&gt;intended decision logic&lt;/strong&gt; by testing strictly against the specification.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. Structural Testing Can’t Detect Missing Logic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Structural testing checks only &lt;strong&gt;what’s written in the code&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;cannot detect what’s missing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A decision table helps reveal &lt;strong&gt;decisions that should exist but don’t&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3. Clarifies Test Coverage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;With a decision table, it’s clear &lt;strong&gt;which conditions and results are being tested&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It makes it easier to find &lt;strong&gt;gaps or redundancies&lt;/strong&gt; in the test cases and enables &lt;strong&gt;better reviews among team members&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Relationship Between Decision Tables and Structural Testing
&lt;/h2&gt;

&lt;p&gt;Creating test cases based on a decision table can indirectly cover code branches (like &lt;code&gt;if&lt;/code&gt; statements).&lt;br&gt;&lt;br&gt;
However, structural testing alone is not enough in cases like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logic using &lt;strong&gt;polymorphism&lt;/strong&gt; (inheritance or dynamic dispatch)&lt;/li&gt;
&lt;li&gt;Conditions depending on &lt;strong&gt;configuration files&lt;/strong&gt; or &lt;strong&gt;database values&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since these aren’t explicitly shown as branches in the code, &lt;strong&gt;they may be missed by structural tests&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
The following section provides concrete examples.&lt;/p&gt;
&lt;h3&gt;
  
  
  When Logic Uses Polymorphism
&lt;/h3&gt;

&lt;p&gt;Polymorphism means that the same method name can behave differently depending on the class. This is a key feature of object-oriented programming.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Java&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;showDiscount&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No discount"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PremiumUser&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;showDiscount&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"20% discount"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// We don't know if it's a regular or premium user&lt;/span&gt;
&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;showDiscount&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// Behavior changes depending on the class at runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the branching is not done with if statements but by the type of class.&lt;br&gt;
So, structural testing (which follows code branches like if) may miss this logic.&lt;/p&gt;
&lt;h3&gt;
  
  
  When Conditions Depend on Config Files or Databases
&lt;/h3&gt;

&lt;p&gt;Sometimes, the behavior of an app is controlled by values in external files, not by conditions written in the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isFeatureEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;enable_feature&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In such cases, model-based testing (like decision tables) lets you verify the logic from a specification point of view.&lt;br&gt;
In other words:&lt;/p&gt;

&lt;p&gt;Structural testing checks whether the code behaves as written.&lt;/p&gt;

&lt;p&gt;Model-based testing checks whether the right behavior or decision is included in the first place, from a user or specification perspective.&lt;/p&gt;

&lt;p&gt;It’s especially useful to detect missing logic or misunderstood design that can’t be seen from code alone.&lt;/p&gt;

&lt;h4&gt;
  
  
  Structural Testing
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Focus&lt;/strong&gt;: Which branches are executed?  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focuses on code structure (if statements, loops)
&lt;/li&gt;
&lt;li&gt;Does not consider the software’s purpose or meaning
&lt;/li&gt;
&lt;li&gt;Can be done with the specification and source code
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Model-Based Testing
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Focus&lt;/strong&gt;: Are the necessary decisions being made?  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires domain knowledge and user perspective
&lt;/li&gt;
&lt;li&gt;Uses models (like decision tables) to represent “what decisions should be made and how”
&lt;/li&gt;
&lt;li&gt;Accessible to non-engineers and upstream stakeholders
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Decision table-based testing offers several practical strengths and benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handles &lt;strong&gt;non-Boolean values&lt;/strong&gt; easily (e.g., capacity = none / 0GB / 8GB / unlimited)
&lt;/li&gt;
&lt;li&gt;Easy to understand for &lt;strong&gt;domain experts&lt;/strong&gt;, helpful for reviews and prioritization
&lt;/li&gt;
&lt;li&gt;Can detect &lt;strong&gt;missing specifications&lt;/strong&gt; that haven't been implemented
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Perspective&lt;/th&gt;
&lt;th&gt;Structural Testing&lt;/th&gt;
&lt;th&gt;Model-Based Testing (Decision Table)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Goal&lt;/td&gt;
&lt;td&gt;Code coverage&lt;/td&gt;
&lt;td&gt;Specification accuracy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required Resources&lt;/td&gt;
&lt;td&gt;Code, specifications&lt;/td&gt;
&lt;td&gt;Requirements, domain knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who Runs It&lt;/td&gt;
&lt;td&gt;Developers, testers&lt;/td&gt;
&lt;td&gt;Upstream members (e.g., consultants)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strength&lt;/td&gt;
&lt;td&gt;Branch coverage&lt;/td&gt;
&lt;td&gt;Detects spec gaps, handles non-Boolean logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Decision tables act as a &lt;strong&gt;bridge&lt;/strong&gt; from upstream to downstream phases.&lt;br&gt;&lt;br&gt;
They clarify specifications, guide design and implementation, and verify that the software behaves &lt;strong&gt;as specified&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
By combining them with structural testing, we can both “build it right” and “build the right thing.”&lt;br&gt;&lt;br&gt;
This dual approach ensures high-quality software by covering both missing logic and implementation mistakes.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>testing</category>
    </item>
    <item>
      <title>Understanding Software Test Coverage Criteria Step by Step: From Line Coverage to MC/DC</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Fri, 07 Mar 2025 04:09:10 +0000</pubDate>
      <link>https://dev.to/yo-shi/understanding-software-test-coverage-criteria-step-by-step-from-line-coverage-to-mcdc-54k0</link>
      <guid>https://dev.to/yo-shi/understanding-software-test-coverage-criteria-step-by-step-from-line-coverage-to-mcdc-54k0</guid>
      <description>&lt;p&gt;Software testing methods in development often have similar names, which can be quite confusing. Understanding their differences helps avoid confusion when implementing tests and makes it easier to explain them to non-testing team members.&lt;/p&gt;

&lt;p&gt;This time, let’s go through five basic coverage criteria in software testing, from fundamental to advanced, step by step. Each of these coverage criteria was developed to address the shortcomings of the previous ones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Line Coverage&lt;/strong&gt; helps identify unexecuted code but doesn’t ensure sufficient testing of conditional branches.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch Coverage&lt;/strong&gt; tests both true and false outcomes of conditions but doesn’t consider the impact of individual conditions.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition Coverage&lt;/strong&gt; checks each condition’s true/false outcomes, but short-circuit evaluation can lead to gaps in coverage.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Condition Coverage&lt;/strong&gt; tests all possible combinations, but the number of test cases can explode.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MC/DC&lt;/strong&gt; (Modified Condition/Decision Coverage) minimizes test cases while ensuring each condition's independence is tested, leading to high-quality test results with fewer tests.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these coverage criteria was designed to compensate for the shortcomings of the previous approach, ensuring better overall test effectiveness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Line (Statement) Coverage
&lt;/h3&gt;

&lt;p&gt;This one is straightforward. It simply measures how much of the code was executed by the test. The formula is &lt;code&gt;number of executed lines / total lines of code&lt;/code&gt;. At first, I wondered if this had any real significance, but it turns out that line coverage is useful in the early stages of development to identify unexecuted parts of the code. Especially for humans, it's easy to understand when test tools highlight executed and unexecuted lines in green and red during source code reviews. However, since line coverage alone is not enough to verify software quality, it is usually combined with branch coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branch (Decision) Coverage
&lt;/h3&gt;

&lt;p&gt;This measures coverage by ensuring that each conditional branch has been executed at least once for both True and False outcomes. The key point here is that the focus is on results.&lt;/p&gt;

&lt;p&gt;For example, given the condition &lt;code&gt;if (A &amp;amp;&amp;amp; B)&lt;/code&gt;, there are four possible combinations, but branch coverage requires only two test cases: No.1 and one from [No.2, No.3, or No.4], since that ensures both True and False outcomes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;No&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When there are multiple conditions, each condition's effect must be checked &lt;strong&gt;independently&lt;/strong&gt;. Consider the following pseudo-code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if A==True {
doSomething1();
}
if B==True {
doSomething2();
}
if C==True {
doSomething3();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code contains three independent branches. To satisfy branch coverage, each conditional branch must be executed for both True and False outcomes. At first glance, it might seem that just two cases, "TTT" and "FFF," would be enough, but that would fail to confirm &lt;strong&gt;independent effects&lt;/strong&gt; of each condition. Without this concept of independence, unnecessary tests could be added, or some test cases might be missing.&lt;/p&gt;

&lt;p&gt;For example, if the test cases were only "TTT" and "FFF," they wouldn’t separately verify the impact of changing only A or only B. The essence of branch coverage is to confirm &lt;strong&gt;the independent behavior of each branch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Therefore, the following four test cases are necessary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;th&gt;C&lt;/th&gt;
&lt;th&gt;if execution paths&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;All &lt;code&gt;if&lt;/code&gt; statements are executed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;if(A)&lt;/code&gt; is skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;if(B)&lt;/code&gt; is skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;if(C)&lt;/code&gt; is skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Condition Coverage
&lt;/h3&gt;

&lt;p&gt;While branch coverage focuses on whether conditional statements like &lt;code&gt;if&lt;/code&gt; and &lt;code&gt;else&lt;/code&gt; are executed at least once, condition coverage focuses on whether changes in each part of the condition affect the outcome. It ensures that each condition is executed at least once as both True and False.&lt;/p&gt;

&lt;p&gt;Let's revisit the example from branch coverage. Given &lt;code&gt;if(A &amp;amp;&amp;amp; B)&lt;/code&gt;, there are four possible combinations. In condition coverage, we need a test pair that ensures A and B each take both True and False values. This can be achieved with either [No.1, No.4] or [No.2, No.3], reducing the required test cases to two.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;No&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A key issue here is &lt;strong&gt;short-circuit evaluation&lt;/strong&gt;. For instance, in &lt;code&gt;if(A &amp;amp;&amp;amp; B)&lt;/code&gt;, if A is &lt;code&gt;False&lt;/code&gt;, B is never evaluated. This is problematic because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;B is not actually tested in execution, making the test incomplete.&lt;/li&gt;
&lt;li&gt;If B has side effects (e.g., function calls or variable modifications), skipping its evaluation can cause behavioral differences that go unnoticed.&lt;/li&gt;
&lt;li&gt;Bugs related to B might remain hidden.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Additionally, condition coverage does not ensure full branch coverage. This limitation leads to the need for more advanced coverage criteria.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple Condition Coverage
&lt;/h3&gt;

&lt;p&gt;Condition coverage alone can leave gaps in test coverage. To avoid this, &lt;strong&gt;multiple condition coverage&lt;/strong&gt; tests all possible combinations of conditions. This is a stricter criterion than condition coverage and eliminates gaps, but it comes with significant drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Exponential growth in test cases&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With N conditions, &lt;strong&gt;2^N&lt;/strong&gt; test cases are required.
&lt;/li&gt;
&lt;li&gt;More conditions mean an explosion in test cases, making it impractical.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Redundant test cases&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It includes cases with identical outcomes.
&lt;/li&gt;
&lt;li&gt;Many tests might be functionally meaningless.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Inefficiency due to short-circuit evaluation&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;code&gt;if(A || B)&lt;/code&gt;, if A is &lt;code&gt;True&lt;/code&gt;, B is never evaluated.
&lt;/li&gt;
&lt;li&gt;This leads to unnecessary tests for conditions that are never executed.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In practice, minimizing test cases while ensuring proper condition testing is key. Condition coverage is often sufficient, but for &lt;strong&gt;high-reliability systems&lt;/strong&gt; such as &lt;strong&gt;aviation and medical software&lt;/strong&gt;, &lt;strong&gt;MC/DC (Modified Condition/Decision Coverage)&lt;/strong&gt; is used.&lt;/p&gt;

&lt;h3&gt;
  
  
  MC/DC (Modified Condition/Decision Coverage)
&lt;/h3&gt;

&lt;p&gt;MC/DC ensures that each condition independently affects the outcome, covering all necessary condition changes with the &lt;strong&gt;minimum number of test cases&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To satisfy MC/DC, the following four conditions must be met:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each condition must take both True and False at least once.
&lt;/li&gt;
&lt;li&gt;The result must take both True and False at least once.
&lt;/li&gt;
&lt;li&gt;Each condition must independently affect the overall result.
&lt;/li&gt;
&lt;li&gt;This must be achieved with the &lt;strong&gt;minimum number of test cases&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This might sound abstract, so let’s go through a concrete example. Suppose we are offering apple juice, and the conditions are whether we have a &lt;strong&gt;cup&lt;/strong&gt;, &lt;strong&gt;juice&lt;/strong&gt;, and &lt;strong&gt;ice&lt;/strong&gt;. To serve apple juice, a &lt;strong&gt;cup&lt;/strong&gt; and &lt;strong&gt;juice&lt;/strong&gt; are absolutely necessary, while &lt;strong&gt;ice&lt;/strong&gt; is optional. The full set of possible combinations is as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;cup&lt;/th&gt;
&lt;th&gt;juice&lt;/th&gt;
&lt;th&gt;ice&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;NG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;NG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;NG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;NG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;NG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;NG&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MC/DC focuses only on &lt;strong&gt;conditions that independently affect the result&lt;/strong&gt;. Since ice does not affect the result, it is &lt;strong&gt;excluded&lt;/strong&gt; from testing. We then check whether changing cup or juice individually alters the result.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Case 1 (&lt;strong&gt;T,T,T&lt;/strong&gt;) is chosen to represent a valid (OK) result.&lt;/li&gt;
&lt;li&gt;Case 4 (&lt;strong&gt;F,T,T&lt;/strong&gt;) tests the effect of &lt;strong&gt;removing the cup&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Case 7 (&lt;strong&gt;T,F,T&lt;/strong&gt;) tests the effect of &lt;strong&gt;removing the juice&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;cup&lt;/th&gt;
&lt;th&gt;juice&lt;/th&gt;
&lt;th&gt;ice&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;NG (cup's effect)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;NG (juice's effect)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With these selected test cases, &lt;strong&gt;MC/DC criteria are satisfied&lt;/strong&gt;. Since MC/DC ensures that each condition independently affects the outcome, it optimizes test cases while considering &lt;strong&gt;short-circuit evaluation&lt;/strong&gt;, making it a highly efficient testing method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Strengths and Weaknesses
&lt;/h2&gt;

&lt;p&gt;For those new to software testing, the numerous similar-sounding test names and definitions can be confusing. Many explanations assume that &lt;strong&gt;each condition independently affects the outcome&lt;/strong&gt;, which makes understanding even harder.&lt;/p&gt;

&lt;p&gt;When creating real-world test cases, balancing &lt;strong&gt;time, cost, and acceptable risk&lt;/strong&gt; is crucial. Having a solid grasp of &lt;strong&gt;what each coverage criterion aims to achieve&lt;/strong&gt; can be valuable for all stakeholders involved in testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.qt.io/product/quality-assurance/coco/feature-line-coverage" rel="noopener noreferrer"&gt;Qt: Line Coverage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rapitasystems.com/mcdc-coverage" rel="noopener noreferrer"&gt;What is MC/DC?&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.parasoft.com/blog/measuring-code-coverage/" rel="noopener noreferrer"&gt;Measuring Code Coverage: Guide to Effective Testing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>beginners</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
