<?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.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>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;   - p=none: Take no action and send a report to the sender.&lt;br&gt;
   - p=quarantine: Place the email in the spam folder (quarantine).&lt;br&gt;
   - 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  
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;Uses customized credentials for the organization instead of default, generic authentication.  &lt;/li&gt;
&lt;li&gt;Accesses resources in a manner that complies with the organization's security policy.  &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.  &lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Log out from the existing authentication.&lt;br&gt;
&lt;br&gt;
  &lt;br&gt;
   &lt;code&gt;bash&lt;br&gt;
   clasp logout&lt;br&gt;
   &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;   &lt;code&gt;bash&lt;br&gt;
   clasp login --creds ./credentials.json&lt;br&gt;
   &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>
    <item>
      <title>What I Learned from Tracking My Child’s YouTube Activity</title>
      <dc:creator>Yoshi</dc:creator>
      <pubDate>Tue, 18 Feb 2025 23:30:00 +0000</pubDate>
      <link>https://dev.to/yo-shi/what-i-learned-from-tracking-my-childs-youtube-activity-2mj2</link>
      <guid>https://dev.to/yo-shi/what-i-learned-from-tracking-my-childs-youtube-activity-2mj2</guid>
      <description>&lt;p&gt;In modern parenting, it’s difficult to completely block YouTube. When I was a child, I had limited options for video content, either watching TV in real-time or viewing pre-recorded videos on VHS or DVD. Today's children, however, are exposed to endless content from platforms like YouTube from a young age. While this has its benefits, such as discovering new genres or learning niche knowledge that even adults may not be aware of, there are times when the sheer addictiveness of YouTube is surprising. Kids ask to watch it during spare moments, and even when we're out and about.&lt;/p&gt;

&lt;p&gt;Until children reach around elementary school age, parents can usually manage their YouTube usage, but once they grow older, they start searching for new videos and exploring recommendations on their own. In our household, we noticed that our child was gradually watching only game YouTubers, and we began to think about what to do next. Fortunately, YouTube allows you to extract tracking data, so I decided to create a tool that would let us monitor their activity by processing that data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concept
&lt;/h2&gt;

&lt;p&gt;The information available from YouTube history includes three main points: title, uploader, and the timestamp of the start time. Due to my professional background, I've created numerous dashboards and reporting systems, and I’ve found that, in most cases, usage tends to decrease over time. When building a reporting system, it's easy to get caught up in visualizations and enriching the data with additional details. However, for this particular purpose, I kept the idea of "simple is best" in mind and refined the necessary information through two perspectives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Viewing Time Perspective&lt;/strong&gt;

&lt;ol&gt;
&lt;li&gt;Total viewing time&lt;/li&gt;
&lt;li&gt;Daily viewing time&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Content Perspective&lt;/strong&gt;

&lt;ol&gt;
&lt;li&gt;Viewing time by channel&lt;/li&gt;
&lt;li&gt;Extraction of undesirable content&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;h3&gt;
  
  
  Viewing Time
&lt;/h3&gt;

&lt;p&gt;Since the only data available was the timestamp of the start time, I calculated the viewing time by comparing the difference with the previous data. I also set a rule to treat any viewing gap of 90 minutes or more as the end of a session. If there was no data for a session end or if only one video was watched in a day, I couldn’t calculate the difference, so I filled in these gaps with the session median or pre-set approximate values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content
&lt;/h3&gt;

&lt;p&gt;Once the viewing time is calculated, viewing time by channel is simply aggregated. For extracting undesirable content, I used the OpenAI API to assess whether a title could potentially harm children born in "yyyy/mm." This judgment is made on a 4-level scale, and titles rated as "Potentially Harmful (Level 3)" or "Harmful (Level 4)" are reported.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;For this project, I used GAS (Google Apps Script), a simple automation tool from Google. While I considered integrating dashboards and linking with messenger apps, the primary goal was to monitor numbers and content. Therefore, I kept it simple and mostly completed the task within Google's ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivery Content
&lt;/h2&gt;

&lt;p&gt;Using GAS, I set up a simple system to send reports to a specified email address with text and graphs attached. When the command is executed, the report content is sent via email to both my wife and me, as shown in the example below (the title/channel names have been changed to fictitious ones).&lt;/p&gt;




&lt;p&gt;Here is your latest YouTube watch time report.&lt;br&gt;
Period: 2025-01-24 (Thu) - 2025-02-13 (Wed)&lt;/p&gt;

&lt;p&gt;++++++++++++++++++++++++++++++&lt;br&gt;
Total Watch Time: 11h 41m&lt;br&gt;
++++++++++++++++++++++++++++++&lt;/p&gt;

&lt;p&gt;■■■ Overview:&lt;br&gt;
YouTube Watch History Analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safe (Level 1): 131 videos&lt;/li&gt;
&lt;li&gt;Mildly Inappropriate (Level 2): 3 videos&lt;/li&gt;
&lt;li&gt;Potentially Harmful (Level 3): 1 videos&lt;/li&gt;
&lt;li&gt;Harmful (Level 4): 0 videos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Collaboration Project&lt;/strong&gt;: "The Hide-and-Seek Game where You are Attacked by a Maniacal Clown at a Private Hotel is Insane... [Screaming]"&lt;/p&gt;

&lt;p&gt;■■■ Top 10 Most-Watched Channels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Craft Life: 6h 30m&lt;/li&gt;
&lt;li&gt;Craft World: 0h 58m&lt;/li&gt;
&lt;li&gt;Game Master XYZ: 0h 51m&lt;/li&gt;
&lt;li&gt;Akane-Iro: 0h 39m&lt;/li&gt;
&lt;li&gt;MEN Play: 0h 32m&lt;/li&gt;
&lt;li&gt;Sports NOW: 0h 24m&lt;/li&gt;
&lt;li&gt;Creators TV: 0h 18m&lt;/li&gt;
&lt;li&gt;Odin Studio: 0h 16m&lt;/li&gt;
&lt;li&gt;Hikaru's Play: 0h 8m&lt;/li&gt;
&lt;li&gt;Football Highlights: 0h 7m&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;■■■ Daily Watch Time:&lt;br&gt;
Date: TotalWatchTime (StartTime)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2025-01-24 (Thu): 0h 40m (11:27)&lt;/li&gt;
&lt;li&gt;2025-01-25 (Fri): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-01-26 (Sat): 0h 57m (08:49)&lt;/li&gt;
&lt;li&gt;2025-01-27 (Sun): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-01-28 (Mon): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-01-29 (Tue): 0h 10m (17:04)&lt;/li&gt;
&lt;li&gt;2025-01-30 (Wed): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-01-31 (Thu): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-02-01 (Fri): 2h 11m (15:52)&lt;/li&gt;
&lt;li&gt;2025-02-02 (Sat): 0h 45m (15:30)&lt;/li&gt;
&lt;li&gt;2025-02-03 (Sun): 1h 23m (15:59)&lt;/li&gt;
&lt;li&gt;2025-02-04 (Mon): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-02-05 (Tue): 0h 21m (17:00)&lt;/li&gt;
&lt;li&gt;2025-02-06 (Wed): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-02-07 (Thu): 1h 9m (14:55)&lt;/li&gt;
&lt;li&gt;2025-02-08 (Fri): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-02-09 (Sat): 2h 26m (06:53)&lt;/li&gt;
&lt;li&gt;2025-02-10 (Sun): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-02-11 (Mon): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-02-12 (Tue): ---- (--:--)&lt;/li&gt;
&lt;li&gt;2025-02-13 (Wed): 1h 39m (15:30)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&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%2Fe447r0zqfnwjqwci7ec1.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%2Fe447r0zqfnwjqwci7ec1.png" alt="watch-time" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Pros and Cons
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Points of Success
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The children's viewing time has been visualized with a reasonable level of quality.

&lt;ul&gt;
&lt;li&gt;It allows discussions with the child based on data, removing subjectivity.&lt;/li&gt;
&lt;li&gt;We could confirm that the child was waking up early in the past to watch YouTube.&lt;/li&gt;
&lt;li&gt;We gained insight into the child’s main viewing channels.&lt;/li&gt;
&lt;li&gt;While I knew the child was watching craft games, it turned out they were mostly watching games.&lt;/li&gt;
&lt;li&gt;Surprisingly, soccer, another hobby, wasn’t being watched much.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;We can check for titles that might have a harmful impact.

&lt;ul&gt;
&lt;li&gt;Since it also affects suggestions, I remove any harmful titles from the history once checked.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Points for Improvement
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Not fully automated.

&lt;ul&gt;
&lt;li&gt;YouTube Takeout can only be scheduled bi-monthly, up to 6 times.&lt;/li&gt;
&lt;li&gt;For monthly reports, extracting data for two consecutive months requires a specified date setting.&lt;/li&gt;
&lt;li&gt;There are also restrictions with the child's account...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Restrictions with the child’s account:

&lt;ul&gt;
&lt;li&gt;Can't automate with GAS for the child’s account.&lt;/li&gt;
&lt;li&gt;Can't forward emails from the child's account either.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Limited information available.

&lt;ul&gt;
&lt;li&gt;Created based on title, uploader, and start time, within the available data range.&lt;/li&gt;
&lt;li&gt;If richer information could be fetched via the YouTube API...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After creating the report, I discussed it with my child and was surprised by how much more they were watching than expected. I also realized that as parents, we allowed continuous YouTube watching due to our own busyness. I plan to prepare better alternatives for the child to spend time outside of videos.&lt;/p&gt;
&lt;h2&gt;
  
  
  Technical Details
&lt;/h2&gt;

&lt;p&gt;Since this was my first time using GAS, I approached it with the principle of simplicity and prioritizing Google services. Below are the services used and their respective purposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google TakeOut&lt;/strong&gt;: Retrieve YouTube data and set up periodic execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documents&lt;/strong&gt;: Transfer viewing data between parent and child, and store various data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spreadsheets&lt;/strong&gt;: Expand JSON data and create graphs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gmail&lt;/strong&gt;: Send report emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GAS&lt;/strong&gt;: Integrate and execute services, as well as perform numerical calculations for the report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt;: Assess the potential danger of video titles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data flow proceeds as follows:&lt;br&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%2Fvdw6jm50mk6au4tb7zod.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%2Fvdw6jm50mk6au4tb7zod.png" alt="data flow" width="748" height="758"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Points to Consider During Implementation
&lt;/h3&gt;

&lt;p&gt;The code is available in &lt;a href="https://github.com/y-oyaizu/watchtracker/tree/main" rel="noopener noreferrer"&gt;this Git repository&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Data Retrieval
&lt;/h4&gt;

&lt;p&gt;Since YouTube is viewed on a child-specific account, the viewing history data is retrieved from the child's account. By accessing Google’s TakeOut service through the child's account, YouTube viewing data can be extracted. The extracted data can then be received by attaching a download link to an email or storing it in various storage services. For this implementation, I chose to store it in Google Documents.&lt;/p&gt;

&lt;p&gt;Unfortunately, there are two obstacles preventing the full automation of the report at this stage, both of which are present in this step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scheduled Distribution Frequency&lt;/strong&gt;: The extraction schedule can only be set for bi-monthly intervals, with a maximum of one year. While one year is acceptable, the bi-monthly frequency creates gaps that are too large, so if you want monthly schedules, you need to set up extraction for two consecutive months.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Child Account Restrictions&lt;/strong&gt;: With a child’s account, automatic email forwarding and the use of GAS are restricted. As a result, ideas like automatically forwarding emails to the parent’s account or automatically placing data in the shared folder for parent-child use in Google Docs couldn’t be used. Instead, manual storage became necessary. However, this can be resolved in the future, as external storage like Box is available for storing the data.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Extracting ZIP Files
&lt;/h3&gt;

&lt;p&gt;The data distributed from TakeOut was in a multi-layered ZIP format, which couldn't be extracted using GAS’s standard utilities. Fortunately, there is a &lt;a href="https://github.com/imaya/zlib.js" rel="noopener noreferrer"&gt;ZIP library&lt;/a&gt; available for use in GAS. Referring to &lt;a href="https://tech.actindi.net/2021/10/01/101244" rel="noopener noreferrer"&gt;this Japanese article&lt;/a&gt;, I was able to create a library and successfully extract the ZIP files.&lt;/p&gt;
&lt;h3&gt;
  
  
  Estimating and Filling Missing Viewing Data
&lt;/h3&gt;

&lt;p&gt;Viewing time is extracted by comparing the difference from the previous data. However, the following issues arose. In the following, I use the term &lt;strong&gt;session&lt;/strong&gt; to refer to a continuous viewing period.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Significant viewing time between sessions, ranging from a few hours to dozens of hours.&lt;/li&gt;
&lt;li&gt;The final timestamp in the data cannot provide a value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To address these issues, I took the following measures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For the final data within a session, I used the median value of the session to fill the gap.&lt;/li&gt;
&lt;li&gt;If there was only one data point within a session, I filled the gap with a provisional value of 20 minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By doing this, although the numbers are estimates, I was able to calculate values that are reasonably close to the actual ones.&lt;/p&gt;
&lt;h3&gt;
  
  
  Harmful Title Evaluation
&lt;/h3&gt;

&lt;p&gt;Although Gemini would be the ideal choice within the Google ecosystem, since I haven't used it much, I decided to leverage OpenAI's API, which I am more familiar with. The threshold for determining harmful content seemed to vary depending on age, so I set the &lt;code&gt;birthYearMonth&lt;/code&gt; in GAS's &lt;code&gt;Script Properties&lt;/code&gt; to adjust the evaluation based on the actual age.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;analyzeWithOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthYearMonth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PropertiesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptProperties&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;❌ API key is missing in Script Properties.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;API key is missing. Please set OPENAI_API_KEY in Script Properties.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;riskCounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1 (Safe)&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2 (Mildly inappropriate)&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3 (Potentially harmful)&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4 (Harmful)&lt;/span&gt;&lt;span class="dl"&gt;"&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;harmfulTitles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;analyzedResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="nx"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Evaluate the following YouTube video title for harmful content for a child born in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;birthYearMonth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.
                                 Classify it into four levels:
                                 - 1 (Safe)
                                 - 2 (Mildly inappropriate)
                                 - 3 (Potentially harmful)
                                 - 4 (Harmful)

                                 Return only the numeric classification.

                                 Title: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GAS Execution Time
&lt;/h3&gt;

&lt;p&gt;When making requests to the OpenAI API, I process each title individually, inserting a 500ms sleep time between each request. Without considering the API request processing, the maximum number of tasks that can be handled on GAS is 720.&lt;/p&gt;

&lt;p&gt;Since the volume was about 72 titles over two weeks, this was not an issue. However, if the usage becomes heavier or if I need to search for more videos like in the case of short videos, it may break down. In the future, I plan to filter videos with over 5 minutes of viewing time, classify titles, and process multiple titles in batches.&lt;/p&gt;

&lt;h3&gt;
  
  
  GAS Module System
&lt;/h3&gt;

&lt;p&gt;I prepared function files for each feature, but GAS lacks a module system, so I had to determine the corresponding module file based on the function name alone. This became a bit cumbersome as the number of files increased. It’s likely because GAS isn't designed for complex applications, but this was a small stumbling point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Plans
&lt;/h2&gt;

&lt;p&gt;Although it’s manual for now, I plan to run the report system at least once a month. This has become a topic for both me and my child to engage with YouTube more thoughtfully. Ideally, the platform would provide such reports as part of its core features, but I’m not holding my breath for that. It's more of a "nice-to-have" feature.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/y-oyaizu/watchtracker" rel="noopener noreferrer"&gt;GitHub Repository: watchtracker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://note.com/cachu_don/n/n976502e18345" rel="noopener noreferrer"&gt;Reflecting on YouTube Viewing History: How Did You End Up Hooked on PokoP? ***JAPANESE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/imaya/zlib.js" rel="noopener noreferrer"&gt;zlib.js GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tech.actindi.net/2021/10/01/101244" rel="noopener noreferrer"&gt;How I Encountered an Error When Trying to Unzip Excel Files in GAS ***JAPANESE&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gas</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
