Skip to content

Home

Caveats with Android In-App Browsers

When you're developing a web application, you need to take browser compatibility issues into account. I encountered a problem with an Android in-app browser that wasn't tested during development but manifested in production, leading to thousands of failed transactions. There were no clear logs to pinpoint the root cause, so it took me some time to figure it out. I'm documenting this issue here in hopes of saving you some future troubleshooting time.

When your end-users access your web app through a third-party Android in-app browser, you have no control over this webView as it is provided by the third party. If the setJavaScriptEnabled method is set to false, you're essentially at a dead-end. If you're lucky enough for the frontend code to still load, note that the setDomStorageEnabled setting is false by default. If you refer to the official documentation:

Android WebSettings setDomStorageEnabled Method

This boolean flag sets whether the DOM storage API is enabled or not. The default value is false, which means the WebView will disable the DOM storage API. This setting can halt your code execution when it tries to access the localStorage object in the browser.

MDN Web Docs on localStorage

The solution is simple: add a condition to check whether localStorage is available before proceeding with the code. This issue doesn't produce a meaningful error message, making troubleshooting particularly challenging, especially when you have to simulate the problem within an Android in-app browser.

One tip for replicating the issue is to download the following tool:

Android WebView Test App on Google Play

This app is quite useful, as it allows you to view console logs within the Android in-app browser.

Another tip for troubleshooting via server logs is to examine the request header's User-Agent Strings. You can identify WebView requests by looking for the wv field in the example header below:

Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/43.0.2357.65 Mobile Safari/537.36

I hope this article helps you and saves you time dealing with this particular caveat.

Caveats with Android In-App Browsers

Welcome back to another episode of Continuous Improvement, the podcast dedicated to helping developers troubleshoot and improve their web applications. I'm your host, Victor, and today we're going to discuss a common browser compatibility issue that can cause headaches during development.

Picture this scenario: you've developed a web application, thoroughly tested it on different browsers, and deployed it to production. But unexpectedly, you encounter a problem specifically with Android in-app browsers that was not caught during development. Thousands of failed transactions start pouring in, leaving you scratching your head.

In today's episode, I want to share my own experience with this issue, hoping to save you valuable troubleshooting time in the future.

The problem lies in the webView of third-party Android in-app browsers, which you have no control over. If the setJavaScriptEnabled method is set to false, you'll find yourself at a dead-end. But even if the frontend code still manages to load, you might run into another obstacle - the setDomStorageEnabled setting is false by default.

To shed some light on this, let's head over to the official Android documentation. According to the documentation for the setDomStorageEnabled method, this boolean flag determines whether the DOM storage API is enabled or not. By default, it's set to false, effectively disabling the DOM storage API. And here's where the trouble begins if your code relies on accessing the localStorage object.

The localStorage object is widely used in web applications to store data locally, but if it's not available due to the disabled DOM storage API, your code execution can come to a screeching halt. The problem is, this issue doesn't produce a clear error message, making troubleshooting particularly challenging, especially within the Android in-app browser environment.

Fortunately, there's a simple solution. You can add a condition to check if localStorage is available before proceeding with your code. By doing this, you can handle the situation gracefully and avoid unexpected errors.

Now, you might be wondering how to replicate this issue and test your solution. One useful tool for this is the Android WebView Test App available on the Google Play Store. This app allows you to view console logs within the Android in-app browser, giving you valuable insights into any errors or issues that may arise.

Additionally, if you're troubleshooting via server logs, examining the request header's User-Agent Strings can be helpful. Look for the wv field in the User-Agent String to identify WebView requests specifically. This information can assist in narrowing down the problematic requests and provide a starting point for further investigation.

In conclusion, web application development is not without its challenges, and browser compatibility is one of the crucial aspects to consider. Android in-app browsers can present unexpected issues, especially when it comes to the settings related to JavaScript and DOM storage.

By being aware of this particular issue and implementing a simple condition to check for the availability of localStorage, you can anticipate and tackle the problem head-on, saving yourself valuable troubleshooting time.

That's all for today's episode of Continuous Improvement. I hope you found this discussion helpful. As always, stay curious, stay resilient, and keep pushing the boundaries of your web development skills.

Android應用內瀏覽器的注意事項

當您正在開發一個網頁應用時,您需要考慮到瀏覽器的兼容性問題。我在開發過程中並未對Android應用內瀏覽器進行測試,但在正式上線以後出現了問題,導致數千次的交易失敗。我並不能從日誌中清楚地找出問題的根源,因此我花了一些時間來解決。我在這裡記錄下了這個問題,希望可以節省您未來的除錯時間。

當您的終端用戶透過第三方的Android應用內瀏覽器來訪問您的網頁應用時,您無法控制這個webView,因為它是由第三方提供的。如果setJavaScriptEnabled方法設置為false,那麼您就陷入了絕境。如果你有幸使前端代碼依舊能加載,請注意setDomStorageEnabled的設置預設為false。如果您參考官方文檔:

Android WebSettings setDomStorageEnabled Method

這個布爾標誌設定DOM存儲API是否啟用。預設值為false,表示WebView將停用DOM存儲API。當它嘗試訪問瀏覽器中的localStorage對象時,此設定可能會阻止您的代碼執行。

MDN Web Docs on localStorage

解決方案很簡單:在執行代碼之前,添加一個條件來檢查localStorage是否可用。這個問題不會產生有意義的錯誤消息,使得除錯變得特別困難,尤其是當您需要在Android應用內瀏覽器中模擬問題時。

複製此問題的一個技巧是下載以下工具:

Android WebView Test App on Google Play

此應用程式非常實用,因為它可以讓您在Android應用內瀏覽器中查看控制台日誌。

另一個通過服務器日誌進行故障排除的技巧是檢查請求頭部的User-Agent Strings。您可以通過檢查下面的範例頭部中的wv字段來識別WebView的請求:

Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/43.0.2357.65 Mobile Safari/537.36

我希望這篇文章能對您有所幫助,並節省您處理這一特殊注意事項所需的時間。

Explaining the Angular Error: 'Expression Changed After It Has Been Checked'

One of my colleagues encountered an error message while developing an Angular frontend application. The error message read:

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'null', Current value: 'true'.

This error occurred while he was developing a back button feature that navigates from the second page to the first. The first page had already been rendered once and needed to be re-rendered with different initial values.

The root cause of this error lies in Angular's change detection mechanism. After each operation, Angular stores the values it used for that operation in the component view's oldValues property. Once all components have been checked, Angular initiates the next digest cycle. However, instead of performing operations, it compares the current values with those stored from the previous cycle.

It's worth noting that this additional level of checking only occurs in development mode. Angular enforces a unidirectional data flow from the top of the component tree to the bottom. No child component is allowed to update the properties of a parent component once the parent's changes have been processed.

To resolve the above issues, possible solutions include using asynchronous updates, such as setTimeout, or manually triggering change detection at the ngAfterViewInit() lifecycle hook with _changeDetectorRef.detectChanges(). The ChangeDetectorRef class provides the following five methods:

abstract class ChangeDetectorRef {
  abstract markForCheck(): void;
  abstract detach(): void;
  abstract detectChanges(): void;
  abstract checkNoChanges(): void;
  abstract reattach(): void;
}

By utilizing these methods, you can manually run change detection and update the child view. My colleague was pleased to find that the error was resolved following this explanation.

Explaining the Angular Error: 'Expression Changed After It Has Been Checked'

Welcome to "Continuous Improvement," the podcast where we explore tips, tricks, and valuable insights to help you enhance your development skills. I'm your host, Victor, and in today's episode, we'll be discussing a common error message that many developers encounter while working with Angular.

So, picture this: one of our colleagues is deep into developing an Angular frontend application when suddenly, an error message pops up on their screen. The message reads: "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked."

Now, this error occurred while our colleague was working on implementing a back button feature. Specifically, it happened when navigating from the second page back to the first. The first page had already been rendered once, but it needed to be re-rendered with different initial values.

The root cause of this error lies within Angular's change detection mechanism. Let me explain how it works. After performing each operation, Angular stores the values it used for that operation in the component view's oldValues property. Once all components have been checked, Angular initiates the next digest cycle. However, at this stage, instead of performing operations, it compares the current values with those stored from the previous cycle.

It's important to note that this additional level of checking only occurs in development mode. Angular enforces a unidirectional data flow from the top of the component tree to the bottom. In other words, once a parent component's changes have been processed, no child component should update the parent's properties.

Now, let's move on to possible solutions for resolving this error. One option is to use asynchronous updates, such as setTimeout, to defer the update until the next cycle. This allows Angular to process the changes without triggering the error.

Another solution involves manually triggering change detection at the ngAfterViewInit() lifecycle hook using the _changeDetectorRef.detectChanges() method. The ChangeDetectorRef class provides several methods for managing change detection, including markForCheck(), detach(), detectChanges(), checkNoChanges(), and reattach().

By utilizing these methods, you can manually run change detection and update the child view. Our colleague was pleased to find that by implementing this workaround, the error message was resolved.

In conclusion, if you come across the "ExpressionChangedAfterItHasBeenCheckedError" in your Angular development, remember that it's essential to respect Angular's unidirectional data flow. You can either use asynchronous updates or manually trigger change detection, and these methods should help you overcome the error.

That's it for today's episode of "Continuous Improvement." We hope that sharing this experience and offering practical solutions will assist you in your own development journey.

If you found this episode helpful and would like to dive deeper into the world of continuous improvement, be sure to check out our blog and subscribe to our podcast for future episodes.

Remember, growth is a continuous process, and we're here to support you every step of the way. Thanks for listening, and until next time, keep coding and keep improving.

解釋 Angular 錯誤:'Expression Changed After It Has Been Checked'

我的一位同事在開發 Angular 前端應用程序時遇到了一個錯誤訊息,錯誤訊息為:

ExpressionChangedAfterItHasBeenCheckedError:Expression在檢查之後已更改。先前的值:'null',當前的值:'true'。

這個錯誤發生在他開發一個從第二頁導航回第一頁的後退按鈕功能時。第一頁已經渲染過一次,並需要以不同的初始值重新渲染。

這個錯誤的根本原因在於Angular的變更檢測機制。每次操作後,Angular都會將用於該操作的值存儲在組件視圖的 oldValues 屬性中。一旦所有組件都已經檢查過,Angular就會啟動下一個摘要週期。但是,它不進行操作,而是將當前值與上一個週期存儲的值進行比較。

值得注意的是,這種額外的檢查只在開發模式中發生。Angular從組件樹的頂部到底部強制執行單向數據流。一旦父組件的變更已經被處理,就不允許任何子組件更新父組件的屬性。

要解決上述問題,可能的解決方案包括使用異步更新,如 setTimeout,或者在 ngAfterViewInit() 的生命週期鉤子中手動觸發變更檢測,利用 _changeDetectorRef.detectChanges()ChangeDetectorRef 類提供以下五種方法:

抽象類 ChangeDetectorRef {
 抽象 markForCheck(): void;
 抽象 detach(): void;
 抽象 detectChanges(): void;
 抽象 checkNoChanges(): void;
 抽象 reattach(): void;
}

通過利用這些方法,您可以手動運行變更檢測並更新子視圖。我的同事很高興發現錯誤在這種解釋之後得到了解決。

Writing Unit Test Cases with Karma for Angular Components

I'd like to make the case for writing unit tests for your Angular web app. Accelerating time to production is not a valid excuse for accumulating technical debt. Here are some compelling reasons to start:

  1. Unit tests help identify issues as early as possible, especially when multiple teams are working on the same codebase, inadvertently introducing bugs. Avoiding middle-of-the-night calls for production support is a worthy goal.

  2. Tests enable you to refactor your code confidently, ensuring that your app continues to function as expected. You can divide your code into manageable, testable units, as opposed to dealing with a monolithic system.

  3. Your company's policy may mandate a certain level of code coverage, often 80% or higher.

If you're new to this, you may not know how to get started or why it's important. Fortunately, Angular makes it easy. To begin, simply run the following command in your project directory:

npm run test

This will open a Chrome browser window at localhost, on port 9876.

Image 1

Click the "Debug" button to initiate testing.

Image 2

At this point, no tests will run because we haven't written any yet. But you can start writing test cases to cover specific, isolated pieces of code. For instance, let's consider a login.component.ts file, which contains a login() method that toggles a boolean flag from false to true:

export class LoginComponent {
  isLogon = false

  login() {
    this.isLogon = true
  }
}

Next, create a file named login.component.spec.ts for your test cases. Write your first test case as follows:

import { async, ComponentFixture, TestBed } from "@angular/core/testing"
import { LoginComponent } from "./login.component"

describe("LoginComponent", () => {
  let component: LoginComponent
  let fixture: ComponentFixture<LoginComponent>

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [LoginComponent],
    }).compileComponents()
  }))

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
  })

  it("should be able to log on", () => {
    component.login()
    expect(component.isLogon).toBeTruthy()
  })
})

Inside your describe() function, you'll find the test cases. Each case is within its own it() function. The aim here is to test if the isLogon flag turns true after the login() method is triggered.

Image 3

Your first test case should pass! If another developer alters your code, your test will catch it:

Image 4

In a real-world scenario, you might make an API call to a server. However, it's crucial not to call the actual API during your test. Instead, you should mock your API call with stub data.

For instance, let's enhance our LoginComponent to make a service call:

import { AuthenticationService } from "../../services/authentication.service"

export class LoginComponent {
  constructor(private authenticationService: AuthenticationService) {}

  isLogon = false

  login() {
    this.authenticationService.login().subscribe(
      data => {
        this.isLogon = true
      },
      error => {
        this.isLogon = false
      }
    )
  }
}

Now your test will fail because the AuthenticationService isn't yet injected into our testing environment. We can fix this as shown below:

import { async, ComponentFixture, TestBed } from "@angular/core/testing"
import { LoginComponent } from "./login.component"
import { AuthenticationService } from "../../services/authentication.service"
import { of } from "rxjs"

const stubData = {
  username: "testing",
}

class FakeAuthenticationService {
  login() {
    return of(stubData)
  }
}

describe("LoginComponent", () => {
  let component: LoginComponent
  let fixture: ComponentFixture<LoginComponent>
  const newFakeAuthenticationService = new FakeAuthenticationService()

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [LoginComponent],
      providers: [
        {
          provide: AuthenticationService,
          useValue: newFakeAuthenticationService,
        },
      ],
    }).compileComponents()
  }))

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
  })

  it("should be able to log on", () => {
    component.login()
    expect(component.isLogon).toBeTruthy()
  })
})

Your test case should now pass!

Image 5

This example is simplified for demonstration purposes, but the key takeaway is that you should not shy away from writing unit tests.

Writing Unit Test Cases with Karma for Angular Components

Welcome to Continuous Improvement, the podcast where we explore ways to enhance your software development practices. I'm your host, Victor, and in today's episode, we'll be discussing the importance of writing unit tests for your Angular web app.

Many developers often rush to release their code as quickly as possible, sacrificing proper testing in the process. But today, we'll make a strong case for why writing unit tests should be an integral part of your development process, even if it means taking a bit more time.

Let's dive right in. The first compelling reason to write unit tests is their ability to identify issues as early as possible. When multiple teams are working on the same codebase, it's not uncommon for bugs to inadvertently find their way into the app. By having comprehensive unit tests in place, these bugs can be caught early on, preventing those dreaded middle-of-the-night support calls.

Additionally, unit tests enable you to refactor your code with confidence. Instead of dealing with a monolithic system, you can divide your code into manageable, testable units. This modular approach makes it easier to maintain and enhance your app over time.

Now, some companies have policies in place that mandate a certain level of code coverage, often 80% or higher. Writing unit tests not only helps you meet these requirements, but it also provides assurance that your code is well-tested and reliable.

If you're new to unit testing, have no fear. Angular makes it easy to get started. Simply run the following command in your project directory:

npm run test

This will open a Chrome browser window at localhost, on port 9876. From there, you can initiate testing by clicking the "Debug" button.

Now, let's discuss how to write a unit test for a specific piece of code. Imagine you have a login.component.ts file with a login() method that toggles a boolean flag from false to true. To test this functionality, create a file named login.component.spec.ts and write your test cases.

Inside the describe() function, you'll find the test cases. Each case is within its own it() function. For example, in our case, we want to test if the isLogon flag turns true after the login() method is triggered.

By running the tests, you'll see that the first test case passes successfully, ensuring that our code functions as expected. Furthermore, if another developer makes changes that impact this functionality, the test will catch it.

It's important to note that when making API calls during testing, you should avoid calling the actual API. Instead, mock your API call with stub data. This ensures that your tests run consistently, regardless of the state of the actual API.

Remember, unit testing is all about creating reliable, independent tests for individual units of code. By following this practice, you can have confidence in the functionality of your app and prevent regressions during the development process.

Well, that's a wrap for today's episode of Continuous Improvement. I hope you've gained some insight into the significance of writing unit tests for your Angular web app. Remember, taking the extra time to write tests will pay off in the long run, ensuring a more robust and maintainable codebase.

If you enjoyed this episode, make sure to subscribe to our podcast for more valuable insights on software development best practices. Until next time, I'm Victor, signing off. Happy coding!

使用Karma為Angular組件撰寫單元測試案例

我想為你的Angular網頁應用程序編寫單元測試。加速生產時間並不能成為積累技術債的合理理由。以下是一些開始的有力理由:

  1. 單元測試幫助盡可能早地識別問題,尤其是當多個團隊在同一個代碼庫上工作,不經意地引入了錯誤。避免在深夜接到生產支持的電話是一個值得追求的目標。

  2. 測試使您能夠自信地重構代碼,確保您的應用程序繼續按預期運行。您可以將您的代碼分割為可管理的,可測試的單元,而不是處理一個龐大的系統。

  3. 您公司的政策可能要求某種程度的代碼覆蓋率,通常為80%或更高。

如果你是新手,你可能不知道如何開始,或者為什麼這個重要。幸運的是,Angular讓它變得很容易。開始,只需在您的項目目錄中運行以下命令:

npm run test

這將在本地主機的9876端口打開一個Chrome瀏覽器窗口。

Image 1

點擊"Debug"按鈕以啟動測試。

Image 2

此時,由於我們還沒有寫任何測試,所以不會有任何測試運行。但是,您可以開始撰寫測試用例以覆蓋特定的,獨立的代碼片段。例如,讓我們考慮一個login.component.ts檔案,其中包含一個login()方法,該方法將一個布爾標誌從假轉為真:

export class LoginComponent {
  isLogon = false

  login() {
    this.isLogon = true
  }
}

接下來,創建一個名為login.component.spec.ts的檔案以撰寫您的測試用例。如下所示撰寫您的第一個測試用例:

import { async, ComponentFixture, TestBed } from "@angular/core/testing"
import { LoginComponent } from "./login.component"

describe("LoginComponent", () => {
  let component: LoginComponent
  let fixture: ComponentFixture<LoginComponent>

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [LoginComponent],
    }).compileComponents()
  }))

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
  })

  it("should be able to log on", () => {
    component.login()
    expect(component.isLogon).toBeTruthy()
  })
})

在您的describe()函式中,您將找到測試用例。每個案例都在其自己的it()函式中。這裡的目標是測試isLogon標誌在觸發login()方法後是否變為真。

Image 3

您的第一個測試用例應該通過!如果另一個開發者修改了您的代碼,您的測試將捕獲它:

Image 4

在現實世界的情境中,您可能會向服務器發送API呼叫。但是,在您的測試期間重要的是不要真的撥打API。您應該用stub數據模擬您的API呼叫。

例如,讓我們加強我們的LoginComponent以進行服務呼叫:

import { AuthenticationService } from "../../services/authentication.service"

export class LoginComponent {
  constructor(private authenticationService: AuthenticationService) {}

  isLogon = false

  login() {
    this.authenticationService.login().subscribe(
      data => {
        this.isLogon = true
      },
      error => {
        this.isLogon = false
      }
    )
  }
}

現在,您的測試將會失敗,因為AuthenticationService尚未注入我們的測試環境。我們可以如下所示修復這個問題:

import { async, ComponentFixture, TestBed } from "@angular/core/testing"
import { LoginComponent } from "./login.component"
import { AuthenticationService } from "../../services/authentication.service"
import { of } from "rxjs"

const stubData = {
  username: "testing",
}

class FakeAuthenticationService {
  login() {
    return of(stubData)
  }
}

describe("LoginComponent", () => {
  let component: LoginComponent
  let fixture: ComponentFixture<LoginComponent>
  const newFakeAuthenticationService = new FakeAuthenticationService()

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [LoginComponent],
      providers: [
        {
          provide: AuthenticationService,
          useValue: newFakeAuthenticationService,
        },
      ],
    }).compileComponents()
  }))

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
  })

  it("should be able to log on", () => {
    component.login()
    expect(component.isLogon).toBeTruthy()
  })
})

您的測試用例現在應該通過!

Image 5

這個例子為了演示的目的而簡化,但是重要的要取的是,你不應該避免撰寫單元測試。

Set Up a Django Server with Apache Virtual Host and Python Virtual Environment

It took me some time to get everything working together, so I'd like to document the steps to save you time in the future.

Firstly, assume that you already have your CentOS/Ubuntu instance running and Python installed. Create a folder for your project and set the appropriate permissions:

    sudo mkdir /opt/yourpath/projects
    sudo chown $USER /opt/yourpath/projects

If you haven't already initialized your project, you can do so with:

    python -m pip install Django
    django-admin startproject APPNAME /opt/yourpath/projects/APPNAME

By default, the server runs on port 8000:

    python manage.py runserver

To prepare your Django server for production, edit the settings.py file with the following settings:

    DEBUG = False
    ALLOWED_HOSTS = ['*']
    STATIC_URL = '/static/'
    STATIC_ROOT = os.path.join(BASE_DIR, 'static')

Then, you can build the static files using:

    python manage.py collectstatic --noinput

Next, serve your web application via the Apache web server. Assuming you installed Apache2 through yum or apt-get, enable virtual hosts for your project and create the following file:

    touch /opt/yourpath/apache2/conf/vhosts/project-vhost.conf

With the content below:

    <IfDefine !IS_APPNAME_LOADED>
      Define IS_APPNAME_LOADED
      WSGIDaemonProcess APPNAME python-home=/opt/yourpath/python python-path=/opt/yourpath/projects/APPNAME
    </IfDefine>
    <VirtualHost 127.0.0.1:80 _default_:80>
    ...

Remember to replace all instances of APPNAME with your Django project name. Then, create another file for HTTPS:

    touch /opt/yourpath/apache2/conf/vhosts/project-https-vhost.conf

And populate it with similar content, replacing APPNAME as appropriate.

After updating the configurations, restart the Apache server. Your Django site should now be operational.

Lastly, isolate Python dependencies within a virtual environment to avoid dependency issues and version conflicts. Inside your project directory, run:

    pip install virtualenv
    virtualenv venv
    source venv/bin/activate

This creates a folder containing all your Python executables. Subsequent pip install commands will affect only this folder. Now, go back and edit project-vhost.conf and project-https-vhost.conf, changing the python-home path to point to the venv folder:

From:

    WSGIDaemonProcess APPNAME python-home=/opt/yourpath/python python-path=/opt/yourpath/projects/APPNAME

To:

    WSGIDaemonProcess APPNAME python-home=/opt/yourpath/projects/APPNAME/venv python-path=/opt/yourpath/projects/APPNAME

Be sure to point the Python home path to the venv folder, not to the /bin executable or the Python location, to avoid a 500 error. If you encounter issues, check the Apache server error log:

    tail /opt/yourpath/apache2/logs/error_log

That's it! Navigate to your public IP address, and you should see your Django page.

P.S. If you encounter a timeout error at the WSGI level:

    Timeout when reading response headers from daemon process

Edit project-vhost.conf and project-https-vhost.conf to add the following line below WSGIDaemonProcess:

    WSGIApplicationGroup %{GLOBAL}

This addition can resolve timeouts caused by Python C extension modules, such as NumPy.