Tommaso Gionfriddo

Director & Principal Engineer @ Adeptask

6 years in project management software

Adeptask

apps

Building in Constrained Environments

Lessons from YouTrack Apps

Building a YouTrack app

Adjusting our mindset 😄

center

Adjusting our mindset 😠

center

What's important in an app?

  • User perception
  • Security
  • Administration complexity

User Perception

  • Cohesiveness
  • User Experience

Security

  • Isolated environments
  • Data residency requirements
  • Vendor trust

Administration complexity

  • Maintenance
  • Support
  • Understanding ROI
  • License management

How do we build an app?

Anatomy of a YouTrack app


---
config:
    flowchart:
        padding: 30
        subGraphTitleMargin: {"top": 10, "bottom": 10}
---
flowchart LR

    classDef frontend fill: #00bcd4, stroke: #00bcd4, color: #000, stroke-width: 3px
    classDef backend fill: #757575, stroke: #757575, color: #fff, stroke-width: 3px
    classDef database fill: #1e88e5, stroke: #1e88e5, color: #fff, stroke-width: 2px
    classDef api fill: #ff4081, stroke: #ff4081, color: #fff, stroke-width: 1px, stroke-dasharray: 3 3
    F
    B
    DB
    class F frontend
    class B backend
    class DB database
%% Frontend block
    subgraph F[Frontend]
    end

%% Backend block
subgraph B[Backend]
subgraph DB[Database]
end
end

    API[REST API]
    class API api
    F <--> API
    API <--> B

Anatomy of a YouTrack app


---
config:
    flowchart:
        padding: 15

---
flowchart LR

    classDef frontend fill: #00bcd4, stroke: #00bcd4, color: #000, stroke-width: 3px
    classDef backend fill: #757575, stroke: #757575, color: #fff, stroke-width: 3px
    classDef sandbox fill: #ffa726, stroke: #ffa726, color: #000, stroke-width: 2px
    classDef database fill: #1e88e5, stroke: #1e88e5, color: #fff, stroke-width: 2px
    classDef module fill: #ff4081, stroke: #ff4081, color: #fff, stroke-width: 2px
    classDef api fill: #ff4081, stroke: #ff4081, color: #fff, stroke-width: 1px, stroke-dasharray: 3 3
    F
    FS
    W
    B
    BS
    WR
    HH
    DB
    I
    A
    GS
    class F frontend
    class B backend
    class BS,FS sandbox
    class DB database
    class I,A gray
    class WR,HH,GS,W module
%% Frontend block
    subgraph F[Frontend]
        subgraph FS[Sandbox]
            W[App Widget]
        end
    end

%% Backend block
    subgraph B[Backend]
        subgraph BS[Sandbox]
            WR[Workflow Rules]
            HH[HTTP Handlers]

        end

        subgraph DB[Database]
            I[Issues]
            A[Articles]
            GS[App Global Storage]

        end
    end


    API2[REST API]
    class API1,API2 api

%% REST API connections



        FS <--> API2

%%    %% I don't think this is really true
%%        F <--> API2

    API2<--> B

    W <--> API2 <--> HH



    BS <--> I
    BS <--> A
    BS <--> GS


Constraints while building

Loading

FOUC
Flash Of Unstyled Content

What loading an app looks like

Loading a simple app

It's never that simple

React

Ring UI

TypeScript

Vite

Waiting for our bundles

<!-- An App entry point -->

<!DOCTYPE html>
<html>
<head>
   <meta charset="UTF-8">
   <title>Presentation Mode</title>
   <script type="module" crossorigin="" src="./assets/presentation-mode-H9qRo0_n.js"></script>
</head>
<body class="plugin">
<div id="root"></div>
</body>
</html>

Loading a complex app

Here's the app!

Screenshot of some app | medium

It gets worse without expectedDimensions

Some text above

Some text below, to demonstrate the point

And is worst in the dark theme

In the UI

center

A quick workaround

Old

Code
<!-- Our old method -->

 <!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Presentation Mode</title>
    <script type="module" crossorigin="" src="./assets/presentation-mode-H9qRo0_n.js"></script>
</head>
<body class="plugin">
<div id="root"></div>
</body>
</html>
Output

Old

Code
<!-- Our old method -->

 <!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Presentation Mode</title>
    <script type="module" crossorigin="" src="./assets/presentation-mode-H9qRo0_n.js"></script>
</head>
<body class="plugin">
<div id="root"></div>
</body>
</html>
Output

New

Code
<!-- Our new method -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Presentation Mode</title>
    <script type="module" crossorigin="" src="./assets/presentation-mode-H9qRo0_n.js"></script>
    <style>
        .loader {
            width: 100%;
            height: 100%;
            position: absolute;
            left:0;
            top: 0;
            background: linear-gradient(90deg, rgba(255, 117, 140, 1), rgba(255, 126, 179, 1));
        }
    </style>
</head>
<body class="plugin">
<div id="root">
    <span class="loader"></span>
</div>
</body>
</html>
Output

New

Code
<!-- Our new method -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Presentation Mode</title>
    <script type="module" crossorigin="" src="./assets/presentation-mode-H9qRo0_n.js"></script>
    <style>
        .loader {
            width: 100%;
            height: 100%;
            position: absolute;
            left:0;
            top: 0;
            background: linear-gradient(90deg, rgba(255, 117, 140, 1), rgba(255, 126, 179, 1));
        }
    </style>
</head>
<body class="plugin">
<div id="root">
    <span class="loader"></span>
</div>
</body>
</html>
Output

End result

End result

An unconventional solution

Quality > quantity


---
config:
    flowchart:
        padding: 30
        subGraphTitleMargin: {"top": 10, "bottom": 10}
---
graph LR
    subgraph Section1[Consolidated]
        A[
index.html] end subgraph Section2[Standard] B[
home.html] C[
main.js] D[
utils.js] E[
styles.css] end

How it looks now

center

Bonus speed boost


---
config:
    gantt:
        numberSectionStyles: 2
        barHeight: 40
        fontSize: 15
        leftPadding: 100
        sectionFontSize: 15
---
gantt

dateFormat HH:mm:ss.SSS
axisFormat %S.%Ls
tickInterval 250ms

section HTML
index.html  :a1, 00:00:00.000, 100ms

section JS
main.js       :b1, after a1, 100ms
utils.js     :b2, after b1, 100ms

section CSS
styles.css    : c1, after b2, 100ms


---
config:
    gantt:
        numberSectionStyles: 2
        barHeight: 40
        fontSize: 15
        leftPadding: 100
        sectionFontSize: 15
---
gantt

dateFormat HH:mm:ss.SSS
axisFormat %S.%Ls
tickInterval 250ms

section HTML
everything.html  :a1, 00:00:00.000, 150ms
 Traditional method :vert, 00:00:00.400,1ms

Bonus speed boost

Single file bundle

Normal bundle

In-app loading

In-app loading


---
config:
  flowchart:
    htmlLabels: false
    padding: 30
    subGraphTitleMargin: {"top": 10, "bottom": 10}
---
flowchart LR

subgraph 2["`Get Backend Data`"]
A("`Settings`")
B("`I18N`")
C("`License Status`")
end



1("`Register App Widget`") -->
2 -->
3("`Get App Specific Data`") -->
4("`Done`")

A-->B-->C

class 2 subGraph 

Waterfall loading

 

---
config:
    gantt:
        numberSectionStyles: 2
        barHeight: 40
        fontSize: 15
        leftPadding: 100
        sectionFontSize: 15
---
gantt
    
    dateFormat  HH:mm:ss.SSS
    axisFormat  
    tickInterval 250ms

    section YouTrack
    Register app widget  :a1, 00:00:00.000, 2ms

    section Standard
    Settings        :b1, after a1, 119ms
    I18N     :b2, after b1, 113ms
    License status    : b3, after b2, 129ms

    section App specific
      OAuth tokens    : c1, after b3, 120ms

Shorten the waterfall

 

---
config:
    gantt:
        numberSectionStyles: 2
        barHeight: 40
        fontSize: 15
        leftPadding: 100
        sectionFontSize: 15
---
gantt
    
    dateFormat  HH:mm:ss.SSS
    axisFormat  %Lms
    tickInterval 250ms

    section YouTrack
    Register app widget (2ms) :a1, 00:00:00.000, 2ms

    section Standard
    Settings (119ms)       :b1, after a1, 119ms
    I18N (113ms)    :b2, after b1, 113ms
    License status (129ms)    : b3, after b2, 129ms

    section App specific
      OAuth tokens (120ms)   : c1, after b3, 120ms

section Total
    (483ms) :active, d1, 00:00:00.000, 483ms
    

Caching

// Cache for 1 minute and allow revalidation for 3 hours
ctx.response.addHeader("cache-control", "max-age=60, stale-while-revalidate=10800");

Caching improvements

 

---
config:
    gantt:
      numberSectionStyles: 2
        
---
gantt
    title With caching
    dateFormat  HH:mm:ss.SSS
    axisFormat  %Lms
    tickInterval 10ms

    section YouTrack 
    Register app widget (2ms) :ao1, 00:00:00.000, 3ms

    section Standard 
    Settings (5ms)       :bo1, after ao1, 5ms
    I18N (8ms)    :bo2, after bo1, 8ms
    License status (8ms)    : bo3, after bo2, 8ms

    section App specific 
      OAuth tokens (8ms)   : co1, after bo3, 8ms
section Total 
    (31ms) :active, do1, 00:00:00.000, 31ms
    Without caching (483ms): milestone, 00:00:00.483,1ms


    

 

---
config:
    gantt:
      numberSectionStyles: 2
        
---
gantt
    title Without caching
    dateFormat  HH:mm:ss.SSS
    axisFormat  %Lms
    tickInterval 250ms

section YouTrack
    Register app widget (2ms) :a1, 00:00:00.000, 2ms

    section Standard
    Settings (119ms)       :b1, after a1, 119ms
    I18N (113ms)    :b2, after b1, 113ms
    License status (129ms)    : b3, after b2, 129ms

    section App specific
      OAuth tokens (120ms)   : c1, after b3, 120ms

section Total
    (483ms) :active, d1, 00:00:00.000, 483ms
    

In the UI

center

Why this is so important

Why this is so important

center

Local storage

Local storage

OAuth flow diagram | medium

Back to the Sandbox

YouTrack app widget iframe

<iframe 
sandbox="allow-scripts allow-downloads allow-top-navigation allow-popups allow-popups-to-escape-sandbox" 
allow="">
</iframe>

Sandbox tokens required for local storage access*

  • allow-storage-access-by-user-activation
  • allow-scripts
  • allow-same-origin

What are our options?

Store the tokens on the server and request them as needed

OR

Don't store them at all.

Server-side storage

Doing things the wrong way

What we know

  • We can send data to the backend in a request
  • Our backend can access the data it receives in a request
  • Our backend can respond with any data it has access to
  • We can cache the response from a request to our backend

Can we use the HTTP cache as a database?

Naive attempt #1

After the OAuth flow

Request

GET /api/.../accessToken?accessToken=myToken123 HTTP/1.1

Response

200 OK
Cache-Control: private, max-age=3600

{
  "accessToken": "myToken123"
}

Naive attempt #1

Retrieving the stored token

Request

GET /api/.../accessToken HTTP/1.1

Response

400 Bad Request

{
  "error": "No accessToken provided",
}

someUrl.com?name=foo

someUrl.com?name=bar

foo

bar

Naive attempt #2

Request

POST /api/.../accessToken HTTP/1.1
{
  "accessToken": "myToken123"
}

Response

200 OK
Cache-Control: private, max-age=3600

{
  "accessToken": "myToken123"
}

Naive attempt #2

POST requests are not cached by the browser

Always give up

Always give up?

x-youtrack-user: Tom

Sometimes give up

Headers

Request

POST /api/.../accessToken HTTP/1.1
x-access-token: myToken123

Response

200 OK
Cache-Control: private, max-age=3600

{
  "accessToken": "myToken123"
}

Headers

Request

POST /api/.../accessToken HTTP/1.1

Response

200 OK (Cached)
Cache-Control: private, max-age=3600

{
  "accessToken": "myToken123"
}

Arbitrary data

/api/.../localCache?storageKey=Foo

{
  "value": "Baz"
}

/api/.../localCache?storageKey=Bar

{
  "value": "Quux"
}

Before

Complexities of storing temporary data

  • +1 user extension property

  • +2 HTTP Handlers (App API Endpoints)

  • - Responsiveness (Waiting for the server)

    • Another backend request on-load

After

Complexities of storing temporary data

  • Is the key unique?

  • ${appName}-${widgetName}-${featureName}

center

@auto-scaling fittingHeader,math math: katex

Background gradient

Font awesome

TODO add more content here

TODO this may not be relevant

TODO Is the bolding good? -better name-

TODO Make this look nicer

TODO make look nicer

TODO make look nicer

TODO make look nicer

TODO I've got no idea what to put here

TODO make look better